This programme uses a genetic algorithm to get the string "Hello, World!"
. It can be summarized as follows:
- Create a random initial population.
- Let the best reproduce and kill the worst until we get a 'good_enough' result.
I used doctest
to verify correctness.
import doctest
import random
def random_char(start=' ',end='|'):
"""
Returns a random char from ' ' [SPACE] to '|'.
>>> random.seed('EXAMPLE')
>>> random_char()
'v'
"""
return chr(random.randint(ord(' '),ord('|')))
def random_string(length):
"""
Returns a random string os the given length.
>>> random.seed('EXAMPLE')
>>> random_string(10)
"vG5iHyPWG'"
"""
return ''.join([random_char() for _ in range(length)])
def create_strings_pool(length,number):
"""
Creates a 'pool' (list) of number strings of the
given length.
>>> random.seed('EXAMPLE')
>>> create_strings_pool(3,4)
['vG5', 'iHy', 'PWG', "'TH"]
"""
return [random_string(length) for _ in range(number)]
def char_difference(char_1,char_2):
"""
Return the absolute difference between the ASCII values
of two chars.
>>> char_difference('a','b')
1
>>> char_difference('a','f')
5
>>> char_difference('#','A')
30
"""
return abs(ord(char_1) - ord(char_2))
def fitness(candidate,result):
"""
Detrmines how much similar the candidate string is
compared to the result. Char distance is counted:
ex. 'cbr' is more similar to 'car' than 'ccr'
because 'b' is nearer to 'a' then 'c'.
>>> fitness('Hfllo','Hello')
1
>>> fitness('! QWE','World')
218
>>> fitness('ccc','abc')
3
"""
fitness = 0
for index,candidate_char in enumerate(candidate):
result_char = result[index]
fitness += char_difference(candidate_char,result_char)
return fitness
def sort_by_fitness(population,result):
"""
Returns a list where the most fit elements are at the start.
>>> sort_by_fitness(['acc','add','asd','acb','aaa','!!!'],'abc')
['acc', 'acb', 'add', 'aaa', 'asd', '!!!']
"""
return list(sorted(population, key = lambda x: fitness(x,result)))
def take_fittest(string_pool,result,elite_number):
"""
Returns tphe elite_number most fit individuals.
>>> take_fittest(['acc','add','asd','acb','aaa','!!!'],'abc',3)
['acc', 'acb', 'add']
"""
sorted_string_pool = sort_by_fitness(string_pool,result)
return sorted_string_pool[:elite_number]
def kill_less_fit(pool,result,must_kill_number):
"""
Returns all the individual but the kill_number worst ones.
>>> kill_less_fit(['acc','add','asd','acb','aaa','!!!'],'abc',2)
['acc', 'acb', 'add', 'aaa']
"""
sorted_string_pool = sort_by_fitness(pool,result)
return sorted_string_pool[:-must_kill_number]
def crossover(father,mother):
"""
Generates a child by crossover, that is mixing the father and
the mother.
>>> random.seed('EXAMPLE')
>>> crossover('Hello','Noble')
'Noble'
# Weird, usually gives a mix.
"""
son = []
for index,father_char in enumerate(father):
mother_char = mother[index]
if random.random() < 0.50:
son.append(father_char)
else:
son.append(mother_char)
return ''.join(son)
def mutate(string,mutation_rate):
"""
Returns a string where each character has (1 - mutation_rate * 0.9) probability
of being randomly changed.
>>> random.seed("EXAMPLE")
>>> mutate("Hello my dear, how are you?",0.7)
"5HPW' my dDarz h7W =re you?"
"""
return ''.join([i if random.random() < (mutation_rate*0.9) else random_char() for i in string])
def make_child(father,mother,mutation_rate):
"""
Generates a child from mother and father using the crossing_over
and then mutates it accordingly to mutation_rate.
>>> random.seed("EXAMPLE")
>>> make_child("Hello","Hulli",0.6)
'Hulli'
"""
off_spring = crossover(father,mother)
if random.random() < mutation_rate:
return mutate(off_spring,mutation_rate)
else:
return off_spring
def genetic_process(result, pool_size, fitness_treshold, mutation_rate,
elite_number, max_generations, logging):
"""
Simulates a gentic evolution.
1) A random population is created
2) The most fit individuals get to reproduce and the worst are killed
until a 'good_enough' individual is born.
Parametres:
@ result: The target string you want to obtain.
@ pool_size: The size of the starting population.
@ fitness_treshold: How much good you want the solution:
0 means you want a perfect solution, a high number
means that even a far solution is ok.
@ mutation_rate: The probability of random mutations happening:
0 means no random mutations
1 means many random mutations
@ elite_number: The number of best individuals that get to reproduce
each generation.
@ max_generations: The maximum number of generations allowed. If a
solution is not found in that many generations, the best individual
so far is returned.
@ logging: True if you want this evolution to print regular output,
else False.
Return value:
@ A string that is inside the fitness_treshold.
>>> random.seed("Example")
>>> genetic_process("ABC", 20, 5, 0.8, 6, 4, True)
Generation number: 0.
So far the best individual is @E2 with a fitness of 21.
The top five is ['@E2', 'H3I', '@Ed', '@@ ', 'A1*'].
Generation number: 1.
So far the best individual is HEI with a fitness of 16.
The top five is ['HEI', 'AE2', '@E2', '@E2', 'H3I'].
Generation number: 2.
So far the best individual is HE@ with a fitness of 13.
The top five is ['HE@', 'HEI', 'AES', 'AE2', '@E2'].
Generation number: 3.
So far the best individual is AEI with a fitness of 9.
The top five is ['AEI', 'HE@', 'HEI', 'AES', 'AES'].
['AEI', 'HE@', 'HEI', 'AES', 'AES']
"""
pool = create_strings_pool(len(result),pool_size)
pool = sort_by_fitness(pool,result)
for generation_number in range(max_generations):
best_individuals = take_fittest(pool,result,elite_number)
# Sometimes I select the second and the third for greater genetic variability
the_best = random.choice(best_individuals[:3])
for individual in best_individuals:
pool.append(make_child(the_best,individual,mutation_rate))
pool = kill_less_fit(pool,result,elite_number)
if logging:
print("""Generation number: {}.
So far the best individual is {} with a fitness of {}.
The top five is {}.\n""".format(
generation_number + 1, take_fittest(pool,result,1)[0],
fitness(take_fittest(pool,result,1)[0],result),
take_fittest(pool,result,5)))
if fitness(take_fittest(pool,result,1)[0],result) < fitness_treshold:
break
return take_fittest(pool,result,5)
def main():
""" Example of use of the genetic algorithm. """
TARGET = "Hello, World!"
POOL_SIZE = 100
FITNESS_TRESHOLD = 4
MUTATION_RATE = 1
ELITE_NUMBER = 20
NUMBER_OF_GENERATIONS = 10**6
LOGGING = True
print((genetic_process(TARGET,POOL_SIZE,FITNESS_TRESHOLD,
MUTATION_RATE,ELITE_NUMBER,
NUMBER_OF_GENERATIONS, LOGGING)))
if __name__ == "__main__":
doctest.testmod()
main()