258 lines
9.1 KiB
Python
258 lines
9.1 KiB
Python
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)
|