CS 467 - Genetic Algorithms
Due: 2020-04-28 5:00p
- Use a genetic algorithm to find a color match
- Accept the assignment on our Github Classroom.
- Download the git repository to your local computer.
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:
- Generate a random pool of candidates
- Assign each candidate a fitness score
- Use the fitness score to assemble a breeding pool
- Build a new pool of candidates through crossover and mutation
- 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.
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.
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.
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
DNA class, you will find two functions that we need to fill in to implement heredity and evolution:
We will write these generically so we could conceptually reuse this class for other scenarios.
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
Start by creating a new Array to store the child's genes. It should be the same length as
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 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).
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
If you look at the
DNA class, you can see that it takes in an argument
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
color(...entity.genes)). You should set up
y variables and increment them to get a grid of squares. Don't use
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
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
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
Finally, add it to the
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.
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.
Commit your changes to git and push them back up to GitHub. I will find them there.