CS 467 - Genetic Algorithms

Due: 2020-04-28 5:00p

Goals

  • Use a genetic algorithm to find a color match

Prerequisites

  1. Accept the assignment on our Github Classroom.
  2. Download the git repository to your local computer.

Assignment

For this practical you are going to implement a simple genetic algorithm similar to the one described in Nature of Code.

Rather than generating a text string, we are going to try to generate a color. This will allow us to produce something with some aesthetic value, even if the process of finding a color is pretty straightforward.

As a reminder, the generic genetic algorithm looks like this:

  1. Generate a random pool of candidates
  2. Assign each candidate a fitness score
  3. Use the fitness score to assemble a breeding pool
  4. Build a new pool of candidates through crossover and mutation
  5. Return to step 2

For our particular problem, the genotype will be three numbers in the range 0-255. So that we have something interesting to look at, the phenotype will be a small square of color corresponding to those three values.

Typically, genetic algorithms are used to find a "solution", however we won't do that with this example. We won't stop when we get a color match or even after a fixed number of generations, we will just let it run so we can see how the better solutions start to dominate the population. We will also have tools to allow us to change the target so we can see how the system adapts to change.

Starter code

I've given you a little bit of starter code. It includes a skeleton for all of the functions you will implement, and a class for holding the genetic data.

At the top are some constants for things like the population size and mutation rate that you are encouraged to play with once you get things working.

In the setup function, I created a pair of color pickers that allow you to set the background color and the target color (you can ignore the background color for now). You will fill in the rest of setup with code creating the initial population.

In the draw function, I've written in the function calls that will form the backbone of the genetic algorithm. You can pretty much leave those alone.

The final thing to notice is the colorDistSq function. Since we are trying to find a color that matches our target, we need a way to quantify how close our guesses are. This function calculates the Euclidean distance between the two colors. Since we don't need to be precise, and we want to save cycles, we will use the square of the distance to avoid doing the square root. Linked to that is the WORST_SCORE constant, which is the farthest away two colors can be. We will use this for normalizing the fitness scores.

Part 1: The DNA

In the DNA class, you will find two functions that we need to fill in to implement heredity and evolution: crossOver and mutate.

We will write these generically so we could conceptually reuse this class for other scenarios.

crossOver

The crossOver function allows our entities to procreate and pass on their genes to a child. It takes one arguments, parent2, which is meant to be another instance of the DNA class.

Start by creating a new Array to store the child's genes. It should be the same length as this.genes.

Now pick a midpoint for the cross over. This should be a random integer between zero and the length of the genes.

Next, write a loop to copy the genes from the parents into the child genes.

Finally, return a new DNA instance, passing it the child's genes.

mutate

The mutate function will introduce random change into the genome. It takes one argument: the mutationRate, which determines if a particular gene will mutate.

We have a number of different options when it comes to mutation. In this instance, we will just generate a new random value in the range 0-255 (so our class isn't entirely generic).

Iterate over this.genes. For each gene, use random() to pick a random number in the range 0-1. If the number is less than mutationRate, mutate the gene.

Part 2: Create the random initial pool

This is pretty straightforward. In setup I initialized the pool variable with a new Array of the right size. Write a for loop to populate it with DNA objects.

If you look at the DNA class, you can see that it takes in an argument genes. The genes property should be an array of values. In our case, this should be an array of three random integers in the range 0-255. So, make the array, create a new DNA object using it and add it to the pool.

Part 3: Evaluate the members of the pool

Now we are going to work on the evaluate function -- this one is less straightforward. In part, this is because we are going to have this function do a little double duty. We will use this function to both display the phenotype and score the fitness.

You will see that I have already set you up with a loop to visit all of the members of the pool. Initially, just draw a small square for each entity. Use the SIZE variable to determine the size of the rectangle. Create a color object using the genes of the entity and use it to to set the fill (Tip: in JavaScript, we have a (spread)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax] operator that allows us to break apart an array into separate arguments: color(...entity.genes)). You should set up x and y variables and increment them to get a grid of squares. Don't use push and pop as we will need to know where the square is drawn later.

Run your code to make sure you have a large collection of randomly colored blocks.

Now we can calculate the score. Figuring out the fitness function for a genetic algorithm is one of the toughest parts of working with them. We are going to build up a metric based on the distance between our new color and the target.

Pass the new color and the targetColor to the colorDistSq function to get the distance between them. We would like high scores to correspond with good attempts. Unfortunately, our distance function provides the opposite. The best possible score would be a distance of 0 (they are the same color). To reverse this, we will subtract our distance from WORST_SCORE. This should give us the largest possible score when we have a match, and the lowest when the guess is the farthest off.

Now we are going to normalize this value to be between 0 and 1. Just divide the score by WORST_SCORE.

The final tweak will be to square this value. This will change the shape of the curve of this function, and favor the scores that are closer to the target.

This value will be the entity's fitness score. Use it to set the score property of the entity.

Run the code again. It should do anything different -- you just want to make sure it doesn't crash.

Part 4: Build the breeding pool

Our next stop is createBreedingPool. I've already given you the breedingPool array -- you just need to fill it up.

We are going to use "roulette wheel" selection. This basically means that (almost) all of the entities are potential breeders, but that the higher scoring ones are more likely.

To do this, we will treat the fitness score sort of like a probability. All of the scores are in the range 0-1. Multiply this number by 100. This is the count of how many instances of the entity will be added to the breeding pool. Entities with a high score will have a lot of copies in the breeding pool, while low scores will only show up a couple of times (and a score of less than 0.01 will take the entity out of contention).

So, write a loop that visits every entity in pool. For each one, find the count by multiplying the score by 100. Then write an inner loop that adds the entity to the breedingPool that many times.

Part 5: Breeding

On to the breed function. I initialized new_pool for you. To make sure that you could see the initial pool earlier, I had this function return pool. Change that to new_pool now.

At this stage, the goal is to create a new generation, so we want the new pool to be the same size as the old pool. Write a loop based on the length of the pool.

Inside, use the random function to select two parents from the breedingPool. Generate a new child by calling the crossover function on the first parent, passing it the second parent as an argument.

Now mutate the child by calling its mutate method with the MUTATION_RATE.

Finally, add it to the pool.

Try it out. You should have a strobing mass of color that quickly dims down to black. Try setting the target color to other things, and watch the pool compensate. You will find that in many cases, it doesn't take long before the target color is reached by at least some nodes. Because of the mutations, there will always be some non-conformists, but you can usually see the dominate color take hold.

Part 6: Change the blend mode

Trying to get the entities to match a preselected color is somewhat interesting, but what if we put it to work doing something marginally more useful?

One of the challenges of working with blend modes (as many of you have discovered) is that it is not always obvious what color you will get. So, let's put our search tool to work. We will set the target color, but our goal now is to figure out which color will get us the target color under the different blend mode.

To do this, we are just going to make two small changes.

In the draw function, before the call to background, set the blend mode to BLEND (this is to make sure the background function works properly a clears the screen in our desired color). Immediately after, set the blend mode to DIFFERENCE. Now, the displayed color is dependant on what is underneath it.

The second change we will make is in evaluate. In order to score our entities, we need to know which color they produced -- not which color they are storing. Use the get function to grab the color from the middle of the square (this is why we needed to know where the squares were drawn). Pass this value to the colorDistSq instead of the color you got from the genes.

Try it out. Try changing the target color and then try changing the background. Admittedly, this doesn't look too much different from the way it did before, but it is somewhat satisfying to know that it is figuring things out something that we didn't know before, rather than just trying to match a value we started with.

That's it! Admittedly, it is less aesthetic than many of the things that we have produced, but it is still interesting to watch how the colors change when you move the target.

Finishing up

Commit your changes to git and push them back up to GitHub. I will find them there.