From fb213f04ddfcf209d1008b6b96cb8d913cd9b142 Mon Sep 17 00:00:00 2001 From: SimpleArt <71458112+SimpleArt@users.noreply.github.com> Date: Tue, 13 Oct 2020 12:48:20 -0400 Subject: [PATCH] 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. --- src/EasyGA.py | 79 ++++++------- src/crossover/crossover_methods.py | 17 ++- src/mutation/mutation_methods.py | 2 +- .../parent_selection_methods.py | 105 ++++++++++++++---- src/run_testing.py | 7 ++ src/structure/chromosome.py | 6 + src/structure/population.py | 61 ++++++++-- .../survivor_selection_methods.py | 23 +++- 8 files changed, 223 insertions(+), 77 deletions(-) diff --git a/src/EasyGA.py b/src/EasyGA.py index aca6929..fb32f51 100644 --- a/src/EasyGA.py +++ b/src/EasyGA.py @@ -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)) diff --git a/src/crossover/crossover_methods.py b/src/crossover/crossover_methods.py index c45ca1a..ff4ce64 100644 --- a/src/crossover/crossover_methods.py +++ b/src/crossover/crossover_methods.py @@ -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: diff --git a/src/mutation/mutation_methods.py b/src/mutation/mutation_methods.py index 36bdd58..8d49190 100644 --- a/src/mutation/mutation_methods.py +++ b/src/mutation/mutation_methods.py @@ -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: diff --git a/src/parent_selection/parent_selection_methods.py b/src/parent_selection/parent_selection_methods.py index b8e8050..371e231 100644 --- a/src/parent_selection/parent_selection_methods.py +++ b/src/parent_selection/parent_selection_methods.py @@ -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) diff --git a/src/run_testing.py b/src/run_testing.py index bcb9568..947272e 100644 --- a/src/run_testing.py +++ b/src/run_testing.py @@ -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() diff --git a/src/structure/chromosome.py b/src/structure/chromosome.py index 2b36311..af4fdd8 100644 --- a/src/structure/chromosome.py +++ b/src/structure/chromosome.py @@ -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 diff --git a/src/structure/population.py b/src/structure/population.py index 65fad47..84def4e 100644 --- a/src/structure/population.py +++ b/src/structure/population.py @@ -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 diff --git a/src/survivor_selection/survivor_selection_methods.py b/src/survivor_selection/survivor_selection_methods.py index f47a107..143cf83 100644 --- a/src/survivor_selection/survivor_selection_methods.py +++ b/src/survivor_selection/survivor_selection_methods.py @@ -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())