Add back files with new names
This commit is contained in:
252
src/crossover/Crossover_Methods.py
Normal file
252
src/crossover/Crossover_Methods.py
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
from EasyGA import function_info
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Round to an integer near x with higher probability
|
||||||
|
# the closer it is to that integer.
|
||||||
|
randround = lambda x: int(x + random.random())
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _check_weight(individual_method):
|
||||||
|
"""Checks if the weight is between 0 and 1 before running.
|
||||||
|
Exception may occur when using ga.adapt, which will catch
|
||||||
|
the error and try again with valid weight.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def new_method(ga, parent_1, parent_2, *, weight = individual_method.__kwdefaults__.get('weight', None)):
|
||||||
|
|
||||||
|
if weight is None:
|
||||||
|
individual_method(ga, parent_1, parent_2)
|
||||||
|
elif 0 < weight < 1:
|
||||||
|
individual_method(ga, parent_1, parent_2, weight = weight)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Weight must be between 0 and 1 when using {individual_method.__name__}.")
|
||||||
|
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _gene_by_gene(individual_method):
|
||||||
|
"""Perform crossover by making a single new chromosome by combining each gene by gene."""
|
||||||
|
|
||||||
|
def new_method(ga, parent_1, parent_2, *, weight = individual_method.__kwdefaults__.get('weight', 'None')):
|
||||||
|
|
||||||
|
ga.population.add_child(
|
||||||
|
individual_method(ga, value_1, value_2)
|
||||||
|
if weight == 'None' else
|
||||||
|
individual_method(ga, value_1, value_2, weight = weight)
|
||||||
|
for value_1, value_2
|
||||||
|
in zip(parent_1.gene_value_iter, parent_2.gene_value_iter)
|
||||||
|
)
|
||||||
|
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
class Population:
|
||||||
|
"""Methods for selecting chromosomes to crossover."""
|
||||||
|
|
||||||
|
|
||||||
|
def sequential(ga, 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for index in range(len(mating_pool)): # for each parent in the mating pool
|
||||||
|
ga.crossover_individual_impl( # apply crossover to
|
||||||
|
mating_pool[index], # the parent and
|
||||||
|
mating_pool[index-1] # the previous parent
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def random(ga, mating_pool):
|
||||||
|
"""Select random pairs from the mating pool.
|
||||||
|
Every parent is paired with a random parent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for parent in mating_pool: # for each parent in the mating pool
|
||||||
|
ga.crossover_individual_impl( # apply crossover to
|
||||||
|
parent, # the parent and
|
||||||
|
random.choice(mating_pool) # a random parent
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Individual:
|
||||||
|
"""Methods for crossing parents."""
|
||||||
|
|
||||||
|
|
||||||
|
@_check_weight
|
||||||
|
def single_point(ga, parent_1, parent_2, *, weight = 0.5):
|
||||||
|
"""Cross two parents by swapping genes at one random point."""
|
||||||
|
|
||||||
|
minimum_parent_length = min(len(parent_1), len(parent_2))
|
||||||
|
|
||||||
|
# Weighted random integer from 0 to minimum parent length - 1
|
||||||
|
swap_index = int(ga.weighted_random(weight) * minimum_parent_length)
|
||||||
|
|
||||||
|
ga.population.add_child(parent_1[:swap_index] + parent_2[swap_index:])
|
||||||
|
ga.population.add_child(parent_2[:swap_index] + parent_1[swap_index:])
|
||||||
|
|
||||||
|
|
||||||
|
@_check_weight
|
||||||
|
def multi_point(ga, parent_1, parent_2, *, weight = 0.5):
|
||||||
|
"""Cross two parents by swapping genes at multiple points."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@_check_weight
|
||||||
|
@_gene_by_gene
|
||||||
|
def uniform(ga, value_1, value_2, *, weight = 0.5):
|
||||||
|
"""Cross two parents by swapping all genes randomly."""
|
||||||
|
return random.choices(gene_pair, cum_weights = [weight, 1])[0]
|
||||||
|
|
||||||
|
|
||||||
|
class Arithmetic:
|
||||||
|
"""Crossover methods for numerical genes."""
|
||||||
|
|
||||||
|
@_gene_by_gene
|
||||||
|
def average(ga, value_1, value_2, *, weight = 0.5):
|
||||||
|
"""Cross two parents by taking the average of the genes."""
|
||||||
|
|
||||||
|
average_value = weight*value_1 + (1-weight)*value_2
|
||||||
|
|
||||||
|
if type(value_1) == type(value_2) == int:
|
||||||
|
average_value = randround(value)
|
||||||
|
|
||||||
|
return average_value
|
||||||
|
|
||||||
|
|
||||||
|
@_gene_by_gene
|
||||||
|
def extrapolate(ga, value_1, value_2, *, weight = 0.5):
|
||||||
|
"""Cross two parents by extrapolating towards the first parent.
|
||||||
|
May result in gene values outside the expected domain.
|
||||||
|
"""
|
||||||
|
|
||||||
|
extrapolated_value = weight*value_1 + (1-weight)*value_2
|
||||||
|
|
||||||
|
if type(value_1) == type(value_2) == int:
|
||||||
|
extrapolated_value = randround(value)
|
||||||
|
|
||||||
|
return extrapolated_value
|
||||||
|
|
||||||
|
|
||||||
|
@_check_weight
|
||||||
|
@_gene_by_gene
|
||||||
|
def random(ga, value_1, value_2, *, weight = 0.5):
|
||||||
|
"""Cross two parents by taking a random integer or float value between each of the genes."""
|
||||||
|
|
||||||
|
value = value_1 + ga.weighted_random(weight) * (value_2-value_1)
|
||||||
|
|
||||||
|
if type(value_1) == type(value_2) == int:
|
||||||
|
value = randround(value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class Permutation:
|
||||||
|
"""Crossover methods for permutation based chromosomes."""
|
||||||
|
|
||||||
|
@_check_weight
|
||||||
|
def ox1(ga, parent_1, parent_2, *, weight = 0.5):
|
||||||
|
"""Cross two parents by slicing out a random part of one parent
|
||||||
|
and then filling in the rest of the genes from the second parent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Too small to cross
|
||||||
|
if len(parent_1) < 2:
|
||||||
|
return parent_1.gene_list
|
||||||
|
|
||||||
|
# Unequal parent lengths
|
||||||
|
if len(parent_1) != len(parent_2):
|
||||||
|
raise ValueError("Parents do not have the same lengths.")
|
||||||
|
|
||||||
|
# Swap with weighted probability so that most of the genes
|
||||||
|
# are taken directly from parent 1.
|
||||||
|
if random.choices([0, 1], cum_weights = [weight, 1]) == 1:
|
||||||
|
parent_1, parent_2 = parent_2, parent_1
|
||||||
|
|
||||||
|
# Extract genes from parent 1 between two random indexes
|
||||||
|
index_2 = random.randrange(1, len(parent_1))
|
||||||
|
index_1 = random.randrange(index_2)
|
||||||
|
|
||||||
|
# Create copies of the gene lists
|
||||||
|
gene_list_1 = [None]*index_1 + parent_1[index_1:index_2] + [None]*(len(parent_1)-index_2)
|
||||||
|
gene_list_2 = list(parent_2)
|
||||||
|
|
||||||
|
input_index = 0
|
||||||
|
|
||||||
|
# For each gene from the second parent
|
||||||
|
for _ in range(len(gene_list_2)):
|
||||||
|
|
||||||
|
# Remove it if it is already used
|
||||||
|
if gene_list_2[-1] in gene_list_1:
|
||||||
|
gene_list_2.pop(-1)
|
||||||
|
|
||||||
|
# Add it if it has not been used
|
||||||
|
else:
|
||||||
|
if input_index == index_1:
|
||||||
|
input_index = index_2
|
||||||
|
gene_list_1[input_index] = gene_list_2.pop(-1)
|
||||||
|
input_index += 1
|
||||||
|
|
||||||
|
ga.population.add_child(gene_list_1)
|
||||||
|
|
||||||
|
|
||||||
|
@_check_weight
|
||||||
|
def partially_mapped(ga, parent_1, parent_2, *, weight = 0.5):
|
||||||
|
"""Cross two parents by slicing out a random part of one parent
|
||||||
|
and then filling in the rest of the genes from the second parent,
|
||||||
|
preserving the ordering of genes wherever possible.
|
||||||
|
|
||||||
|
NOTE: Needs to be fixed, since genes are not hashable..."""
|
||||||
|
|
||||||
|
# Too small to cross
|
||||||
|
if len(parent_1) < 2:
|
||||||
|
return parent_1.gene_list
|
||||||
|
|
||||||
|
# Unequal parent lengths
|
||||||
|
if len(parent_1) != len(parent_2):
|
||||||
|
raise ValueError("Parents do not have the same lengths.")
|
||||||
|
|
||||||
|
# Swap with weighted probability so that most of the genes
|
||||||
|
# are taken directly from parent 1.
|
||||||
|
if random.choices([0, 1], cum_weights = [weight, 1]) == 1:
|
||||||
|
parent_1, parent_2 = parent_2, parent_1
|
||||||
|
|
||||||
|
# Extract genes from parent 1 between two random indexes
|
||||||
|
index_2 = random.randrange(1, len(parent_1))
|
||||||
|
index_1 = random.randrange(index_2)
|
||||||
|
|
||||||
|
# Create copies of the gene lists
|
||||||
|
gene_list_1 = [None]*index_1 + parent_1[index_1:index_2] + [None]*(len(parent_1)-index_2)
|
||||||
|
gene_list_2 = list(parent_2)
|
||||||
|
|
||||||
|
# Create hash for gene list 2
|
||||||
|
hash = {gene:index for index, gene in enumerate(gene_list_2)}
|
||||||
|
|
||||||
|
# For each gene in the copied segment from parent 2
|
||||||
|
for i in range(index_1, index_2):
|
||||||
|
|
||||||
|
# If it is not already copied,
|
||||||
|
# find where it got displaced to
|
||||||
|
j = i
|
||||||
|
while gene_list_1[(j := hash[gene_list_1[j]])] is not None:
|
||||||
|
pass
|
||||||
|
gene_list_1[j] = gene_list_2[i]
|
||||||
|
|
||||||
|
# Fill in whatever is leftover (copied from ox1).
|
||||||
|
# For each gene from the second parent
|
||||||
|
for _ in range(len(gene_list_2)):
|
||||||
|
|
||||||
|
# Remove it if it is already used
|
||||||
|
if gene_list_2[-1] in gene_list_1:
|
||||||
|
gene_list_2.pop(-1)
|
||||||
|
|
||||||
|
# Add it if it has not been used
|
||||||
|
else:
|
||||||
|
if input_index == index_1:
|
||||||
|
input_index = index_2
|
||||||
|
gene_list_1[input_index] = gene_list_2.pop(-1)
|
||||||
|
input_index += 1
|
||||||
|
|
||||||
|
ga.population.add_child(gene_list_1)
|
||||||
1
src/crossover/README.md
Normal file
1
src/crossover/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Mutation functions
|
||||||
0
src/crossover/__init__.py
Normal file
0
src/crossover/__init__.py
Normal file
0
src/crossover/test_crossover_methods.py
Normal file
0
src/crossover/test_crossover_methods.py
Normal file
46
src/fitness_function/Fitness_Examples.py
Normal file
46
src/fitness_function/Fitness_Examples.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
def is_it_5(chromosome):
|
||||||
|
"""A very simple case test function - If the chromosome's gene value
|
||||||
|
is equal to 5 add one to the chromosomes overall fitness value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Overall fitness value
|
||||||
|
fitness = 0
|
||||||
|
|
||||||
|
for gene in chromosome:
|
||||||
|
|
||||||
|
# Increment fitness is the gene's value is 5
|
||||||
|
if gene.value == 5:
|
||||||
|
fitness += 1
|
||||||
|
|
||||||
|
return fitness
|
||||||
|
|
||||||
|
|
||||||
|
def near_5(chromosome):
|
||||||
|
"""Test's the GA's ability to handle floats. Computes how close each gene is to 5."""
|
||||||
|
|
||||||
|
# Overall fitness value
|
||||||
|
fitness = 0
|
||||||
|
|
||||||
|
for gene in chromosome:
|
||||||
|
|
||||||
|
# Add squared distance to 5
|
||||||
|
fitness += (5 - gene.value) ** 2
|
||||||
|
|
||||||
|
return fitness
|
||||||
|
|
||||||
|
|
||||||
|
def index_dependent_values(chromosome):
|
||||||
|
"""Test of the GA's ability to improve fitness when the value is index-dependent.
|
||||||
|
If a gene is equal to its index in the chromosome + 1, fitness is incremented.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Overall fitness value
|
||||||
|
fitness = 0
|
||||||
|
|
||||||
|
for i, gene in enumerate(chromosome):
|
||||||
|
|
||||||
|
# Increment fitness is the gene's value is i+1
|
||||||
|
if gene.value == i+1:
|
||||||
|
fitness += 1
|
||||||
|
|
||||||
|
return fitness
|
||||||
1
src/fitness_function/README.md
Normal file
1
src/fitness_function/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Fitness function
|
||||||
0
src/fitness_function/__init__.py
Normal file
0
src/fitness_function/__init__.py
Normal file
1
src/fitness_function/test_methods.py
Normal file
1
src/fitness_function/test_methods.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
208
src/mutation/Mutation_Methods.py
Normal file
208
src/mutation/Mutation_Methods.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
from EasyGA import function_info
|
||||||
|
import random
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _check_chromosome_mutation_rate(population_method):
|
||||||
|
"""Checks if the chromosome mutation rate is a float between 0 and 1 before running."""
|
||||||
|
|
||||||
|
def new_method(ga):
|
||||||
|
|
||||||
|
if not isinstance(ga.chromosome_mutation_rate, float):
|
||||||
|
raise TypeError("Chromosome mutation rate must be a float.")
|
||||||
|
|
||||||
|
elif 0 < ga.chromosome_mutation_rate < 1:
|
||||||
|
population_method(ga)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError("Chromosome mutation rate must be between 0 and 1.")
|
||||||
|
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _check_gene_mutation_rate(individual_method):
|
||||||
|
"""Checks if the gene mutation rate is a float between 0 and 1 before running."""
|
||||||
|
|
||||||
|
def new_method(ga, index):
|
||||||
|
|
||||||
|
if not isinstance(ga.gene_mutation_rate, float):
|
||||||
|
raise TypeError("Gene mutation rate must be a float.")
|
||||||
|
|
||||||
|
elif 0 < ga.gene_mutation_rate <= 1:
|
||||||
|
individual_method(ga, index)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError("Gene mutation rate must be between 0 and 1.")
|
||||||
|
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _reset_fitness(individual_method):
|
||||||
|
"""Resets the fitness value of the chromosome."""
|
||||||
|
|
||||||
|
def new_method(ga, chromosome):
|
||||||
|
chromosome.fitness = None
|
||||||
|
individual_method(ga, chromosome)
|
||||||
|
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _loop_random_mutations(individual_method):
|
||||||
|
"""Runs the individual method until enough
|
||||||
|
genes are mutated on the indexed chromosome.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Change input to include the gene index being mutated.
|
||||||
|
def new_method(ga, chromosome):
|
||||||
|
|
||||||
|
sample_space = range(len(chromosome))
|
||||||
|
sample_size = ceil(len(chromosome)*ga.gene_mutation_rate)
|
||||||
|
|
||||||
|
# Loop the individual method until enough genes are mutated.
|
||||||
|
for index in random.sample(sample_space, sample_size):
|
||||||
|
individual_method(ga, chromosome, index)
|
||||||
|
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
class Population:
|
||||||
|
"""Methods for selecting chromosomes to mutate"""
|
||||||
|
|
||||||
|
@_check_chromosome_mutation_rate
|
||||||
|
def random_selection(ga):
|
||||||
|
"""Selects random chromosomes."""
|
||||||
|
|
||||||
|
sample_space = range(len(ga.population))
|
||||||
|
sample_size = ceil(len(ga.population)*ga.chromosome_mutation_rate)
|
||||||
|
|
||||||
|
# Loop the individual method until enough genes are mutated.
|
||||||
|
for index in random.sample(sample_space, sample_size):
|
||||||
|
ga.mutation_individual_impl(ga.population[index])
|
||||||
|
|
||||||
|
|
||||||
|
@_check_chromosome_mutation_rate
|
||||||
|
def random_avoid_best(ga):
|
||||||
|
"""Selects random chromosomes while avoiding the best chromosomes. (Elitism)"""
|
||||||
|
|
||||||
|
sample_space = range(ceil(ga.percent_converged*len(ga.population)*3/16), len(ga.population))
|
||||||
|
sample_size = ceil(ga.chromosome_mutation_rate*len(ga.population))
|
||||||
|
|
||||||
|
for index in random.sample(sample_space, sample_size):
|
||||||
|
ga.mutation_individual_impl(ga.population[index])
|
||||||
|
|
||||||
|
|
||||||
|
@_check_chromosome_mutation_rate
|
||||||
|
def best_replace_worst(ga):
|
||||||
|
"""Selects the best chromosomes, copies them, and replaces the worst chromosomes."""
|
||||||
|
|
||||||
|
mutation_amount = ceil(ga.chromosome_mutation_rate*len(ga.population))
|
||||||
|
|
||||||
|
for i in range(mutation_amount):
|
||||||
|
ga.population[-i-1] = ga.make_chromosome(ga.population[i])
|
||||||
|
ga.mutation_individual_impl(ga.population[-i-1])
|
||||||
|
|
||||||
|
|
||||||
|
class Individual:
|
||||||
|
"""Methods for mutating a single chromosome."""
|
||||||
|
|
||||||
|
@_check_gene_mutation_rate
|
||||||
|
@_reset_fitness
|
||||||
|
@_loop_random_mutations
|
||||||
|
def individual_genes(ga, chromosome, index):
|
||||||
|
"""Mutates random genes by making completely new genes."""
|
||||||
|
|
||||||
|
# Using the chromosome_impl
|
||||||
|
if ga.chromosome_impl is not None:
|
||||||
|
chromosome[index] = ga.make_gene(ga.chromosome_impl()[index])
|
||||||
|
|
||||||
|
# Using the gene_impl
|
||||||
|
elif ga.gene_impl is not None:
|
||||||
|
chromosome[index] = ga.make_gene(ga.gene_impl())
|
||||||
|
|
||||||
|
# Exit because no gene creation method specified
|
||||||
|
else:
|
||||||
|
raise Exception("Did not specify any initialization constraints.")
|
||||||
|
|
||||||
|
|
||||||
|
class Arithmetic:
|
||||||
|
"""Methods for mutating a chromosome by numerically modifying the genes."""
|
||||||
|
|
||||||
|
@_check_gene_mutation_rate
|
||||||
|
@_reset_fitness
|
||||||
|
@_loop_random_mutations
|
||||||
|
def average(ga, chromosome, index):
|
||||||
|
"""Mutates random genes by making completely new genes
|
||||||
|
and then averaging them with the old genes. May cause
|
||||||
|
premature convergence. Weight is the reciprocal of the
|
||||||
|
number of generations run."""
|
||||||
|
|
||||||
|
weight = 1/max(1, ga.current_generation)
|
||||||
|
|
||||||
|
# Using the chromosome_impl
|
||||||
|
if ga.chromosome_impl is not None:
|
||||||
|
new_value = ga.chromosome_impl()[index]
|
||||||
|
|
||||||
|
# Using the gene_impl
|
||||||
|
elif ga.gene_impl is not None:
|
||||||
|
new_value = ga.gene_impl()
|
||||||
|
|
||||||
|
# Exit because no gene creation method specified
|
||||||
|
else:
|
||||||
|
raise Exception("Did not specify any initialization constraints.")
|
||||||
|
|
||||||
|
chromosome[index] = ga.make_gene((1-weight)*chromosome[index].value + weight*new_value)
|
||||||
|
|
||||||
|
|
||||||
|
@_check_gene_mutation_rate
|
||||||
|
@_reset_fitness
|
||||||
|
@_loop_random_mutations
|
||||||
|
def reflect_genes(ga, chromosome, index):
|
||||||
|
"""Reflects genes against the best chromosome.
|
||||||
|
Requires large genetic variety to work well but
|
||||||
|
when it does it may be very fast."""
|
||||||
|
|
||||||
|
difference = ga.population[0][index].value - chromosome[index].value
|
||||||
|
value = ga.population[0][index].value + 2*difference
|
||||||
|
chromosome[index] = ga.make_gene(value)
|
||||||
|
|
||||||
|
|
||||||
|
class Permutation:
|
||||||
|
"""Methods for mutating a chromosome by changing the order of the genes."""
|
||||||
|
|
||||||
|
@_check_gene_mutation_rate
|
||||||
|
@_reset_fitness
|
||||||
|
@_loop_random_mutations
|
||||||
|
def swap_genes(ga, chromosome, index):
|
||||||
|
"""Swaps two random genes in the chromosome."""
|
||||||
|
|
||||||
|
# Indexes of genes to swap
|
||||||
|
index_one = index
|
||||||
|
index_two = random.randrange(len(chromosome))
|
||||||
|
|
||||||
|
# Swap genes
|
||||||
|
chromosome[index_one], chromosome[index_two] = chromosome[index_two], chromosome[index_one]
|
||||||
|
|
||||||
|
|
||||||
|
@_check_gene_mutation_rate
|
||||||
|
@_reset_fitness
|
||||||
|
def swap_segments(ga, chromosome):
|
||||||
|
"""Splits the chromosome into 3 segments and shuffle them."""
|
||||||
|
|
||||||
|
# Chromosome too short to mutate
|
||||||
|
if len(chromosome) < 3:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Indexes to split the chromosome
|
||||||
|
index_two = random.randrange(2, len(chromosome))
|
||||||
|
index_one = random.randrange(1, index_two)
|
||||||
|
|
||||||
|
# Extract segments and shuffle them
|
||||||
|
segments = [chromosome[:index_one], chromosome[index_one:index_two], chromosome[index_two:]]
|
||||||
|
random.shuffle(segments)
|
||||||
|
|
||||||
|
# Put segments back together
|
||||||
|
chromosome.gene_list = segments[0] + segments[1] + segments[2]
|
||||||
1
src/mutation/README.md
Normal file
1
src/mutation/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Mutation functions
|
||||||
0
src/mutation/__init__.py
Normal file
0
src/mutation/__init__.py
Normal file
0
src/mutation/test_mutation_methods.py
Normal file
0
src/mutation/test_mutation_methods.py
Normal file
257
src/parent_selection/Parent_Selection.py
Normal file
257
src/parent_selection/Parent_Selection.py
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
from EasyGA import function_info
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _check_selection_probability(selection_method):
|
||||||
|
"""Raises an exception if the selection_probability
|
||||||
|
is not between 0 and 1 inclusively. Otherwise runs
|
||||||
|
the selection method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def new_method(ga):
|
||||||
|
if 0 <= ga.selection_probability <= 1:
|
||||||
|
selection_method(ga)
|
||||||
|
else:
|
||||||
|
raise Exception("Selection probability must be between 0 and 1 to select parents.")
|
||||||
|
|
||||||
|
new_method.__name__ = selection_method.__name__
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _check_positive_fitness(selection_method):
|
||||||
|
"""Raises an exception if the population contains a
|
||||||
|
chromosome with negative fitness. Otherwise runs
|
||||||
|
the selection method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def new_method(ga):
|
||||||
|
if ga.get_chromosome_fitness(0) > 0 and ga.get_chromosome_fitness(-1) >= 0:
|
||||||
|
selection_method(ga)
|
||||||
|
else:
|
||||||
|
raise Exception("Converted fitness values can't have negative values or be all 0. Consider using rank selection or stochastic selection instead.")
|
||||||
|
|
||||||
|
new_method.__name__ = selection_method.__name__
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _ensure_sorted(selection_method):
|
||||||
|
"""Sorts the population by fitness
|
||||||
|
and then runs the selection method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def new_method(ga):
|
||||||
|
ga.population.sort_by_best_fitness(ga)
|
||||||
|
selection_method(ga)
|
||||||
|
|
||||||
|
new_method.__name__ = selection_method.__name__
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _compute_parent_amount(selection_method):
|
||||||
|
"""Computes the amount of parents
|
||||||
|
needed to be selected, and passes it
|
||||||
|
as another argument for the method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def new_method(ga):
|
||||||
|
parent_amount = max(2, round(len(ga.population)*ga.parent_ratio))
|
||||||
|
selection_method(ga, parent_amount)
|
||||||
|
|
||||||
|
new_method.__name__ = selection_method.__name__
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
class Rank:
|
||||||
|
"""Methods for selecting parents based on their rankings in the population
|
||||||
|
i.e. the n-th best chromosome has a fixed probability of being selected,
|
||||||
|
regardless of their chances"""
|
||||||
|
|
||||||
|
@_check_selection_probability
|
||||||
|
@_ensure_sorted
|
||||||
|
@_compute_parent_amount
|
||||||
|
def tournament(ga, parent_amount):
|
||||||
|
"""
|
||||||
|
Will make tournaments of size tournament_size and choose the winner (best fitness)
|
||||||
|
from the tournament and use it as a parent for the next generation. The total number
|
||||||
|
of parents selected is determined by parent_ratio, an attribute to the GA object.
|
||||||
|
May require many loops if the selection probability is very low.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Choose the tournament size.
|
||||||
|
# Use no less than 5 chromosomes per tournament.
|
||||||
|
tournament_size = int(len(ga.population)*ga.tournament_size_ratio)
|
||||||
|
if tournament_size < 5:
|
||||||
|
tournament_size = min(5, len(ga.population))
|
||||||
|
|
||||||
|
# Repeat tournaments until the mating pool is large enough.
|
||||||
|
while len(ga.population.mating_pool) < parent_amount:
|
||||||
|
|
||||||
|
# Generate a random tournament group and sort by fitness.
|
||||||
|
tournament_group = sorted(random.sample(
|
||||||
|
range(len(ga.population)),
|
||||||
|
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) ^ index
|
||||||
|
# Each chromosome is (1-selection_probability) times
|
||||||
|
# more likely to become a parent than the next ranked.
|
||||||
|
if random.random() < ga.selection_probability * (1-ga.selection_probability) ** index:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Use random in tournament if noone wins
|
||||||
|
else:
|
||||||
|
index = random.randrange(tournament_size)
|
||||||
|
|
||||||
|
ga.population.set_parent(tournament_group[index])
|
||||||
|
|
||||||
|
|
||||||
|
@_check_selection_probability
|
||||||
|
@_ensure_sorted
|
||||||
|
@_compute_parent_amount
|
||||||
|
def stochastic_geometric(ga, parent_amount):
|
||||||
|
"""
|
||||||
|
Selects parents with probabilities given by a geometric progression. This
|
||||||
|
method is similar to tournament selection, but doesn't create several
|
||||||
|
tournaments. Instead, it assigns probabilities to each rank and selects
|
||||||
|
the entire mating pool using random.choices. Since it essentially uses the
|
||||||
|
entire population as a tournament repeatedly, it is less likely to select
|
||||||
|
worse parents than tournament selection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Set the weights of each parent based on their rank.
|
||||||
|
# Each chromosome is (1-selection_probability) times
|
||||||
|
# more likely to become a parent than the next ranked.
|
||||||
|
weights = [
|
||||||
|
(1-ga.selection_probability) ** i
|
||||||
|
for i
|
||||||
|
in range(len(ga.population))
|
||||||
|
]
|
||||||
|
|
||||||
|
# Set the mating pool.
|
||||||
|
ga.population.mating_pool = random.choices(ga.population, weights, k = parent_amount)
|
||||||
|
|
||||||
|
|
||||||
|
@_check_selection_probability
|
||||||
|
@_ensure_sorted
|
||||||
|
@_compute_parent_amount
|
||||||
|
def stochastic_arithmetic(ga, parent_amount):
|
||||||
|
"""
|
||||||
|
Selects parents with probabilities given by an arithmetic progression. This
|
||||||
|
method is similar to stochastic-geometric selection, but is more likely to
|
||||||
|
select worse parents with its simpler selection scheme.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Set the weights of each parent based on their rank.
|
||||||
|
# The worst chromosome has a weight of 1,
|
||||||
|
# the next worst chromosome has a weight of 2,
|
||||||
|
# etc.
|
||||||
|
# with an inflation of (1-selection probability) * average weight
|
||||||
|
|
||||||
|
average_weight = (len(ga.population)+1) // 2
|
||||||
|
inflation = (1-ga.selection_probability) * average_weight
|
||||||
|
|
||||||
|
weights = [
|
||||||
|
i + inflation
|
||||||
|
for i
|
||||||
|
in range(len(ga.population), 0, -1)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Set the mating pool.
|
||||||
|
ga.population.mating_pool = random.choices(ga.population, weights, k = parent_amount)
|
||||||
|
|
||||||
|
|
||||||
|
class Fitness:
|
||||||
|
|
||||||
|
@_check_selection_probability
|
||||||
|
@_ensure_sorted
|
||||||
|
@_check_positive_fitness
|
||||||
|
@_compute_parent_amount
|
||||||
|
def roulette(ga, parent_amount):
|
||||||
|
"""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. 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 sum of all the fitnessess in a population
|
||||||
|
fitness_sum = sum(
|
||||||
|
ga.get_chromosome_fitness(index)
|
||||||
|
for index
|
||||||
|
in range(len(ga.population))
|
||||||
|
)
|
||||||
|
|
||||||
|
# A list of ranges that represent the probability of a chromosome getting chosen
|
||||||
|
probability = [ga.selection_probability]
|
||||||
|
|
||||||
|
# The chance of being selected increases incrementally
|
||||||
|
for index in range(len(ga.population)):
|
||||||
|
probability.append(probability[-1]+ga.get_chromosome_fitness(index)/fitness_sum)
|
||||||
|
|
||||||
|
probability = probability[1:]
|
||||||
|
|
||||||
|
# Loops until it reaches a desired mating pool size
|
||||||
|
while len(ga.population.mating_pool) < parent_amount:
|
||||||
|
|
||||||
|
# Spin the roulette
|
||||||
|
rand_number = random.random()
|
||||||
|
|
||||||
|
# Find where the roulette landed.
|
||||||
|
for index in range(len(probability)):
|
||||||
|
if (probability[index] >= rand_number):
|
||||||
|
ga.population.set_parent(index)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
@_check_selection_probability
|
||||||
|
@_ensure_sorted
|
||||||
|
@_compute_parent_amount
|
||||||
|
def stochastic(ga, parent_amount):
|
||||||
|
"""
|
||||||
|
Selects parents using the same probability approach as roulette selection,
|
||||||
|
but doesn't spin a roulette for every selection. Uses random.choices with
|
||||||
|
weighted values to select parents and may produce duplicate parents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# All fitnesses are the same, select randomly.
|
||||||
|
if ga.get_chromosome_fitness(-1) == ga.get_chromosome_fitness(0):
|
||||||
|
offset = 1-ga.get_chromosome_fitness(-1)
|
||||||
|
|
||||||
|
# Some chromosomes have negative fitness, shift them all into positives.
|
||||||
|
elif ga.get_chromosome_fitness(-1) < 0:
|
||||||
|
offset = -ga.get_chromosome_fitness(-1)
|
||||||
|
|
||||||
|
# No change needed.
|
||||||
|
else:
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
# Set the weights of each parent based on their fitness + offset.
|
||||||
|
weights = [
|
||||||
|
ga.get_chromosome_fitness(index) + offset
|
||||||
|
for index
|
||||||
|
in range(len(ga.population))
|
||||||
|
]
|
||||||
|
|
||||||
|
inflation = sum(weights) * (1 - ga.selection_probability)
|
||||||
|
|
||||||
|
# Rescale and adjust using selection_probability so that
|
||||||
|
# if selection_probability is high, a low inflation is used,
|
||||||
|
# making selection mostly based on fitness.
|
||||||
|
# if selection_probability is low, a high offset is used,
|
||||||
|
# so everyone has a more equal chance.
|
||||||
|
weights = [
|
||||||
|
weight + inflation
|
||||||
|
for weight
|
||||||
|
in weights
|
||||||
|
]
|
||||||
|
|
||||||
|
# Set the mating pool.
|
||||||
|
ga.population.mating_pool = random.choices(ga.population, weights, k = parent_amount)
|
||||||
1
src/parent_selection/README.md
Normal file
1
src/parent_selection/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Selection functions
|
||||||
0
src/parent_selection/__init__.py
Normal file
0
src/parent_selection/__init__.py
Normal file
1
src/survivor_selection/README.md
Normal file
1
src/survivor_selection/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Selection functions
|
||||||
51
src/survivor_selection/Survivor_Selection.py
Normal file
51
src/survivor_selection/Survivor_Selection.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from EasyGA import function_info
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _append_to_next_population(survivor_method):
|
||||||
|
"""Appends the selected chromosomes to the next population."""
|
||||||
|
|
||||||
|
def new_method(ga):
|
||||||
|
ga.population.append_children(survivor_method(ga))
|
||||||
|
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
@_append_to_next_population
|
||||||
|
def fill_in_best(ga):
|
||||||
|
"""Fills in the next population with the best chromosomes from the last population"""
|
||||||
|
|
||||||
|
needed_amount = len(ga.population) - len(ga.population.next_population)
|
||||||
|
return ga.population[:needed_amount]
|
||||||
|
|
||||||
|
|
||||||
|
@_append_to_next_population
|
||||||
|
def fill_in_random(ga):
|
||||||
|
"""Fills in the next population with random chromosomes from the last population"""
|
||||||
|
|
||||||
|
needed_amount = len(ga.population) - len(ga.population.next_population)
|
||||||
|
return random.sample(ga.population, needed_amount)
|
||||||
|
|
||||||
|
|
||||||
|
@_append_to_next_population
|
||||||
|
def fill_in_parents_then_random(ga):
|
||||||
|
"""Fills in the next population with all parents followed by random chromosomes from the last population"""
|
||||||
|
|
||||||
|
# Remove dupes from the mating pool
|
||||||
|
mating_pool = set(ga.population.mating_pool)
|
||||||
|
|
||||||
|
needed_amount = len(ga.population) - len(ga.population.next_population)
|
||||||
|
parent_amount = min(needed_amount, len(mating_pool))
|
||||||
|
random_amount = needed_amount - parent_amount
|
||||||
|
|
||||||
|
# Only parents are used.
|
||||||
|
if random_amount == 0:
|
||||||
|
return ga.population.mating_pool[:parent_amount]
|
||||||
|
|
||||||
|
# Parents need to be removed from the random sample to avoid dupes.
|
||||||
|
else:
|
||||||
|
return mating_pool + random.sample(
|
||||||
|
set(ga.population) - mating_pool,
|
||||||
|
random_amount
|
||||||
|
)
|
||||||
0
src/survivor_selection/__init__.py
Normal file
0
src/survivor_selection/__init__.py
Normal file
0
src/survivor_selection/test_survivor_methods.py
Normal file
0
src/survivor_selection/test_survivor_methods.py
Normal file
1
src/termination_point/README.md
Normal file
1
src/termination_point/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Termination functions
|
||||||
90
src/termination_point/Termination_Methods.py
Normal file
90
src/termination_point/Termination_Methods.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
from EasyGA import function_info
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _add_by_fitness_goal(termination_impl):
|
||||||
|
"""Adds termination by fitness goal to the method."""
|
||||||
|
|
||||||
|
def new_method(ga):
|
||||||
|
|
||||||
|
# Try to check the fitness goal
|
||||||
|
try:
|
||||||
|
|
||||||
|
# If minimum fitness goal reached, stop ga.
|
||||||
|
if ga.target_fitness_type == 'min' and ga.population[0].fitness <= ga.fitness_goal:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If maximum fitness goal reached, stop ga.
|
||||||
|
elif ga.target_fitness_type == 'max' and ga.population[0].fitness >= ga.fitness_goal:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Fitness or fitness goals are None
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Population not initialized
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check other termination methods
|
||||||
|
return termination_impl(ga)
|
||||||
|
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _add_by_generation_goal(termination_impl):
|
||||||
|
"""Adds termination by generation goal to the method."""
|
||||||
|
|
||||||
|
def new_method(ga):
|
||||||
|
|
||||||
|
# If generation goal is set, check it.
|
||||||
|
if ga.generation_goal is not None and ga.current_generation >= ga.generation_goal:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check other termination methods
|
||||||
|
return termination_impl(ga)
|
||||||
|
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
@function_info
|
||||||
|
def _add_by_tolerance_goal(termination_impl):
|
||||||
|
"""Adds termination by tolerance goal to the method."""
|
||||||
|
|
||||||
|
def new_method(ga):
|
||||||
|
|
||||||
|
# If tolerance is set, check it, if possible.
|
||||||
|
try:
|
||||||
|
best_fitness = ga.population[0].fitness
|
||||||
|
threshhold_fitness = ga.population[round(ga.percent_converged*len(ga.population))].fitness
|
||||||
|
tol = ga.tolerance_goal * (1 + abs(best_fitness))
|
||||||
|
|
||||||
|
# Terminate if the specified amount of the population has converged to the specified tolerance
|
||||||
|
if abs(best_fitness - threshhold_fitness) < tol:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Fitness or tolerance goals are None
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Population not initialized
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check other termination methods
|
||||||
|
return termination_impl(ga)
|
||||||
|
|
||||||
|
return new_method
|
||||||
|
|
||||||
|
|
||||||
|
@_add_by_fitness_goal
|
||||||
|
@_add_by_generation_goal
|
||||||
|
@_add_by_tolerance_goal
|
||||||
|
def fitness_generation_tolerance(ga):
|
||||||
|
"""Terminate GA when any of the
|
||||||
|
- fitness,
|
||||||
|
- generation, or
|
||||||
|
- tolerance
|
||||||
|
goals are met."""
|
||||||
|
|
||||||
|
return True
|
||||||
0
src/termination_point/__init__.py
Normal file
0
src/termination_point/__init__.py
Normal file
0
src/termination_point/test_termination_methods.py
Normal file
0
src/termination_point/test_termination_methods.py
Normal file
Reference in New Issue
Block a user