Delete files for renaming
This commit is contained in:
@ -1 +0,0 @@
|
||||
# Mutation functions
|
||||
@ -1,2 +0,0 @@
|
||||
# FROM (. means local) file_name IMPORT function_name
|
||||
from .crossover_methods import Crossover_Methods
|
||||
@ -1,258 +0,0 @@
|
||||
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 Crossover_Methods:
|
||||
|
||||
# Allowing access to decorators when importing class
|
||||
_check_weight = _check_weight
|
||||
_gene_by_gene = _gene_by_gene
|
||||
|
||||
|
||||
class Population:
|
||||
"""Methods for selecting chromosomes to crossover."""
|
||||
|
||||
|
||||
def sequential_selection(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_selection(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 +0,0 @@
|
||||
# Fitness function
|
||||
@ -1,2 +0,0 @@
|
||||
# FROM (. means local) file_name IMPORT class name
|
||||
from .fitness_examples import Fitness_Examples
|
||||
@ -1,36 +0,0 @@
|
||||
class Fitness_Examples:
|
||||
"""Fitness function examples used"""
|
||||
|
||||
def is_it_5(chromosome):
|
||||
"""A very simple case test function - If the chromosomes gene value is a 5 add one
|
||||
to the chromosomes overall fitness value."""
|
||||
# Overall fitness value
|
||||
fitness = 0
|
||||
# For each gene in the chromosome
|
||||
for gene in chromosome:
|
||||
# Check if its value = 5
|
||||
if(gene.value == 5):
|
||||
# If its value is 5 then add one to
|
||||
# the overal fitness of the chromosome.
|
||||
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.
|
||||
"""
|
||||
return sum([1-pow(1-gene.value/5, 2) for gene in chromosome])
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
fitness = 0
|
||||
for i in range(len(chromosome)):
|
||||
if (chromosome[i].value == i+1):
|
||||
fitness += 1
|
||||
|
||||
return fitness
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1 +0,0 @@
|
||||
# Mutation functions
|
||||
@ -1,2 +0,0 @@
|
||||
# FROM (. means local) file_name IMPORT function_name
|
||||
from .mutation_methods import Mutation_Methods
|
||||
@ -1,217 +0,0 @@
|
||||
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 Mutation_Methods:
|
||||
|
||||
_check_chromosome_mutation_rate = _check_chromosome_mutation_rate
|
||||
_check_gene_mutation_rate = _check_gene_mutation_rate
|
||||
_reset_fitness = _reset_fitness
|
||||
_loop_random_mutations = _loop_random_mutations
|
||||
|
||||
|
||||
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 +0,0 @@
|
||||
# Selection functions
|
||||
@ -1,2 +0,0 @@
|
||||
# FROM (. means local) file_name IMPORT function_name
|
||||
from .parent_selection_methods import Parent_Selection
|
||||
@ -1,266 +0,0 @@
|
||||
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 Parent_Selection:
|
||||
|
||||
# Allowing access to decorators when importing class
|
||||
_check_selection_probability = _check_selection_probability
|
||||
_check_positive_fitness = _check_positive_fitness
|
||||
_ensure_sorted = _ensure_sorted
|
||||
_compute_parent_amount = _compute_parent_amount
|
||||
|
||||
|
||||
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 +0,0 @@
|
||||
# Selection functions
|
||||
@ -1,2 +0,0 @@
|
||||
# FROM (. means local) file_name IMPORT function_name
|
||||
from .survivor_selection_methods import Survivor_Selection
|
||||
@ -1,60 +0,0 @@
|
||||
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))
|
||||
|
||||
new_method.__name__ = survivor_method.__name__
|
||||
return new_method
|
||||
|
||||
|
||||
class Survivor_Selection:
|
||||
"""Survivor selection determines which individuals should be brought to the next generation"""
|
||||
|
||||
# Allowing access to decorators when importing class
|
||||
_append_to_next_population = _append_to_next_population
|
||||
|
||||
|
||||
@_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
|
||||
)
|
||||
@ -1 +0,0 @@
|
||||
# Termination functions
|
||||
@ -1,2 +0,0 @@
|
||||
# FROM (. means local) file_name IMPORT class name
|
||||
from .termination_methods import Termination_Methods
|
||||
@ -1,99 +0,0 @@
|
||||
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
|
||||
|
||||
|
||||
class Termination_Methods:
|
||||
"""Example functions that can be used to terminate the the algorithms loop"""
|
||||
|
||||
# Allowing access to decorators when importing class
|
||||
_add_by_fitness_goal = _add_by_fitness_goal
|
||||
_add_by_generation_goal = _add_by_generation_goal
|
||||
_add_by_tolerance_goal = _add_by_tolerance_goal
|
||||
|
||||
|
||||
@_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
|
||||
Reference in New Issue
Block a user