Genetic algorithm
A genetic algorithm is a search technique based on the principle of evolution. It starts out with a random population of possible solutions (in this example: black&white grids). Each new generation, the fittest members of the population “mate” and create a new population of children. In this case, the fitness of a member is based on symmetry: “if I am a black square, is my horizontal and vertical mirror also a black square?”.
Here is a random member from the first generation: And here is the fittest member of the 60th generation of ancestors: Although we aren’t there yet you can clearly see the pattern arising in the last generation.
Read more: http://en.wikipedia.org/wiki/Genetic_algorithm
Source code:
# Grid size. rows = 15 cols = 15 #----------------------------------------------------------------------------- # A grid represented as a DNA list of True/False values. # True means black, False means white. # There are cols*rows elements in the list. # Obviously, in more complex versions a list could contain all sorts of things, # as long as our fitness function knows how to compare them. def dna(): """ Returns a list of True (1) or False (0) values. """ return [choice((True, False)) for i in range(rows*cols)] #----------------------------------------------------------------------------- # Draw grid command. def draw_grid(dna, width=10): for x in range(cols): for y in range(rows): is_filled = dna[x+y*cols] fill(int(not is_filled)) rect(x*width, y*width, width, width) #----------------------------------------------------------------------------- # Grid symmetry functions. def in_grid(i): """ Is the element at position i within the bounds of the list? """ return i > 0 and i < rows*cols def north(i) : return i-cols # element above def south(i) : return i+cols # element below def east(i) : return i+1 # element to the left def west(i) : return i-1 # element to the right def northeast(i) : return north(east(i)) def northwest(i) : return north(west(i)) def southeast(i) : return south(east(i)) def southwest(i) : return south(west(i)) def row_reflection(i): """ Returns the list index of the element in the grid that is horizontally symmetrical to this one. For example: 0 1 2 3 4 5 6 7 row_reflection(4) returns 7 row_reflection(5) returns 6 """ row = i/cols col = i-row*cols return row*cols + cols-col-1 def col_reflection(i): """ 0 1 2 3 4 5 6 7 col_reflection(1) returns 5 col_reflection(7) returns 3 """ row = i/cols col = i-row*cols return (rows-row-1)*cols + col #----------------------------------------------------------------------------- # GA functions. def recombine(dna1, dna2, crossover=0.5): """ Returns a list that combines the head of dna1 and the tail of dna2. http://en.wikipedia.org/wiki/Crossover_(genetic_algorithm) """ i = int(len(dna1) * crossover) return dna1[:i] + dna2[i:] def fitness(dna): """ Symmetry fitness. Black elements in the grid that have a horizontal and vertical reflection score better. You can use the grid symmetry functions to create all sorts of fitness checks. """ score = 0 for i in range(len(dna)): j = row_reflection(i) if in_grid(j) and dna[j] == dna[i]: score += 1 j = col_reflection(i) if in_grid(j) and dna[j] == dna[i]: score += 1 # Here's another fitness function that will converge to entirely black: #if dna[i] == True: # score += 1 return score def population(size=500): """ Returns a collection of grid DNA lists. """ return [dna() for i in range(size)] def sort_by_fitness(population): """ The DNA lists with the highest fitness will be at the front. """ best = [(fitness(x), x) for x in population] best.sort(reverse=True) return best def next_generation(population, top=0.7, determinism=0.8): """ Returns a new population of children by recombination. http://en.wikipedia.org/wiki/Genetic_algorithm """ # Roughly the fittest top 60% are allowed to reproduce. # We say roughly, because good solutions may be cast aside. # This keeps the population diverse, so it doesn't converge too fast # (less fit DNA might have some portions that become interesting later on). population = sort_by_fitness(population) parents = list(population) i = len(parents) iterations = 0 while len(parents) > len(population)*top: if random() < determinism: parents.pop(i-1) i -= 1 if i == 0: i = len(parents) # I don't like while-loops. # They tend to run forever when there is a bug. # If the loop has been going for 1,000,000 times, # something must have gone wrong. iterations += 1 if iterations > 1000000: break # Cross-combine pairs of parents to a new population of children. # We get the best results with a random crossover. children = [] for i in range(len(population)): parent1 = choice(parents) parent2 = choice(parents) if parent1 == parent2: parent2 = choice(parents) children.append(recombine(parent1[1], parent2[1], crossover=random())) return children from random import seed seed(0) # We need some number-crunching power: import psyco psyco.full() # The initial random population: p = population(size=500) # Process n generations: for i in range(80): p = next_generation(p, top=0.6, determinism=0.7) # A list of scores for each member in the final population: #print [score for score, x in sort_by_fitness(p)] # The best solution in the final population: draw_grid(p[0], width=20)
Improvements
- bigger population size: more diversity = more potential
- less determinism: more randomness = slower convergence to local optimum
- more generations: human evolution took millions of years too