Basic Evolution Algorithm

Evolution algorithms, also known as evolutionary algorithms, are a subset of artificial intelligence that are inspired by the process of natural selection. They are a type of optimization algorithm that uses mechanisms inspired by biological evolution, such as reproduction, mutation, and selection, to find solutions to problems. Evolution algorithms are often used in machine learning and other fields to find the best possible solutions to complex problems.


There are several different types of evolution algorithms, including genetic algorithms, evolutionary programming, and evolutionary strategies. These algorithms typically involve several key steps, including:


  1. Initialization: In this step, a population of potential solutions to the problem is created. These solutions, also known as individuals or chromosomes, are typically represented as strings of binary digits or other data structures.
  2. Evaluation: In this step, each individual in the population is evaluated according to a predefined fitness function. This function measures how well each individual solves the problem, and determines which individuals are more likely to be selected for reproduction.
  3. Selection: In this step, the fittest individuals from the population are selected to reproduce and create a new generation of individuals. The selection process typically involves some form of "survival of the fittest," where the fittest individuals are more likely to be selected for reproduction.
  4. Crossover: In this step, pairs of selected individuals are combined to produce offspring that inherit characteristics from both parents. This is typically done using a crossover operator, which randomly selects bits from each parent to create the offspring.
  5. Mutation: In this step, random changes, or mutations, are introduced into the population to introduce new diversity and prevent the algorithm from getting stuck in a local optimum.
  6. Repeat: These steps are repeated for a predefined number of generations or until a satisfactory solution is found.
Here is an example of a simple evolution algorithm implemented in Python:
Non-Vectorized Implementation
 import random

max_num_gen = 1000 # maximum number of generations
err = 0.0001 # if a candidate solution is within `acc` from the true solution
            # then it is good enough and the search should be terminated
population_size = 10000
survivors_percentage = 0.01
mutation_value = 0.01 # specify by how much candidate solutions should be 
                      # mutated to generate new solutions

# function to optimize
def foo(x, y, z):
  return 6*x**3 + 9*y**2 + 90*z - 51

def fitness(x,y,z):
  '''
  The fitness function assigns a value to a candidate solution indicating its 
  quality (how good this candidate solution is).
  '''
  diff = foo(x,y,z)
  # The fitness is the reciprocal of the returned value
  return 1/(abs(diff)+1e-7)

## Initialization
solutions = []
for s in range(max_num_gen):
  solutions.append((random.uniform(0,population_size), \
                    random.uniform(0,population_size), \
                    random.uniform(0,population_size)) )
  

## Evaluation
for gen in range(max_num_gen):
  rankedsolutions = []
  for s in solutions:
    # evaluate candidate solutions
    rankedsolutions.append( (fitness( s[0],s[1],s[2] ), s) )
    # sort the solutions according to their fitness values in a descending order 
  rankedsolutions.sort(reverse=True)
  # print(rankedsolutions[:4])

  # print the best solution
  print(f"Generation {gen}; Best solution {rankedsolutions[0]}")
 
  # check if the termination condition is met
  if rankedsolutions[0][0] >= 1/err:
    print('The termination condition has been met!')
    break

  ## Selection of the fittest 
  survivors = int(survivors_percentage * population_size)
  bestsolutions = rankedsolutions[:survivors]

  # drop the fitness values
  elements = []
  for s in bestsolutions: 
    elements.append(s[1][0]) 
    elements.append(s[1][1]) 
    elements.append(s[1][2])  

  ## Mutation
  # generate a new population (or generation) by mutating the best solutions
  # from the previous generation
  newGen = []
  for _ in range(population_size):
    e1 = random.choice(elements) * random.uniform(1-mutation_value, 1 + mutation_value)
    e2 = random.choice(elements) * random.uniform(1-mutation_value, 1 + mutation_value)
    e3 = random.choice(elements) * random.uniform(1-mutation_value, 1 + mutation_value)

    newGen.append((e1,e2,e3))

  solutions = newGen
 
Vectorized Implementation of the Evolution Algorithm
 import numpy as np

max_num_gen = 1000 # maximum number of generations
err = 0.0001 # if a candidate solution is within `acc` from the true solution
            # then it is good enough and the search should be terminated
population_size = 1000
survivors_percentage = 0.01
mutation_value = 0.01 # specify by how much candidate solutions should be 
                      # mutated to generate new solutions

## Function to optimize
def foo(s):
  return 6*s[:,0]**3 + 9*s[:,1]**2 + 90*s[:,2] - 51

def fitness(s):
  '''
  The fitness function assigns a value to a candidate solution indicating its 
  quality (how good this candidate solution is).
  '''
  diff = foo(s)
  # make the fitness values equal the receprocal of the value returned by
  # the `foo` function
  return 1/(np.abs(diff)+1e-7)

## Initialization
solutions = np.random.rand(population_size,3) 

## Evaluation
for gen in range(max_num_gen):
  solutions = np.column_stack( (fitness(solutions), solutions))

  # sort the solutions along the first column (using their fitness)
  # if you don't understand the next step check 
  # https://stackoverflow.com/questions/2828059/sorting-arrays-in-numpy-by-column
  rankedsolutions = solutions[ solutions[:, 0].argsort()[::-1] ] 
  print(f"Generation {gen}; Best solution {rankedsolutions[0]}")

  # check if the termination condition is met
  if rankedsolutions[0, 0] >= 1/err:
    print('The termination condition has been met!')
    break

  ## Selection of the fittest 
  num_survivors = int(survivors_percentage * population_size)
  survivors = rankedsolutions[:num_survivors, 1:]

  ## Generate new population
  newGen = survivors[np.random.randint(survivors.shape[0],size=population_size)]
  mutations = np.random.uniform(low=1-mutation_value, high=1+mutation_value,\
                      size=(population_size,3))
  solutions = np.multiply(newGen, mutations)

 
Contact
Gmail ID: amjad.y.majid
Mobile: +31616955224


RoboMind | TU Delft