Project 3 — Genetic Algorithms

Undergrad portion

The program should accept the following as input:

1. Initialize the population. Generate N bit strings each of size l. It is easier to represent the individuals as character arrays rather than integers.

For each generation, do the following:

2. Calculate the fitness of each individual. This can be done in several steps:

a. Find the integer that the individual's bit string represents. Iterate through each bit, and if the bit is a 1, then add the corresponding power of 2 to a running sum. For example, if l=20, and if the leftmost bit of the string is 1, then add 219 to the sum. The power of 2 that you are currently on is a function of the loop index. After looping through all the bits, this running sum will be the integer value of the bit string.

b. Compute the individual's fitness value using the fitness function F(s)=(x/2l)10 where x is the integer value from step a.

c. When you are looping through each individual finding the fitness value, keep a running sum of the total fitness, which is the sum of fitness values for all N individuals.

d. For each individual, normalize its fitness value by dividing its fitness value by the total fitness from step c. It's best to store these normalized values in a separate array than the actual fitness values since you will need to keep the original fitness values for finding the statistics in a later step.

e. For each individual, compute a number which is the sum of that individual's normalized fitness value, and the normalized fitness values for each of the individuals before it. Thus, a running total is kept of the normalized fitness values. This will be helpful later when probabilistically selecting parents.

The following example should make steps d and e more clear. Note that the total fitness value is 1.95. Also note that the sum of all the normalized fitness values is 1.
Individual  Fitness value  Normalized fitness value  Running total
    0           0.05              0.0256                0.0256
    1           0.2               0.1026                0.1282
    2           0.6               0.3077                0.4359
    3           0.3               0.1538                0.5897
    4           0.8               0.4103                1.0000
Now you are set up to select parents and produce the next generation.

Perform N/2 iterations doing the following:

3. Select two individuals to be parents. First get two random numbers between 0 and 1. Then see which range the random numbers fall into based on the running total numbers computed in step 2e above. The random number will be between two of those numbers. The individual associated with the second of those numbers will be the individual selected to be a parent. For example, suppose your two random numbers are 0.4147 and 0.7395. In the example above, the number 0.4147 is between 0.1282 and 0.4359, so individual 2 is one of the parents. In the above example, the number 0.7395 is between 0.5897 and 1.0000, so individual 4 is the other parent. Be sure that you get two distinct parents when you do this step, so that you do not result in an individual mating with itself. Keep selecting a second parent until you get one that is different from the first parent.

4. Mate parents and perform any crossover to get offspring.

a. First generate a random number to determine whether crossover will be done.

b. If no crossover is done, then simply copy the bit strings of the parents into new bit strings which will represent the offspring.

c. If crossover is done, then first randomly select a bit to be the crossover point. Then copy the bit strings of the parents into the bit strings of the offspring up to the crossover point. After the crossover point, reverse which offspring gets the bits from which parent.

5. Perform any mutations on the offspring. For each of the two offspring, go through all the bits in their strings. For each bit, generate a random number to indicate whether that bit will be mutated. If the bit will be mutated, then simply flip that bit, that is, change a 0 to a 1 and a 1 to a 0.

6. Update the population. After obtaining all N new offspring in the above loop, copy all of these offspring bit string arrays into the bit string arrays of the current population. These new offspring will replace the current population. In other words, the current population arrays will be overwritten by the offspring arrays.

7. Find the statistics of the population. Find the average fitness of the population, the fitness of the best individual, and the number of correct bits in the best individual. Find these three measures for each generation. You will use this data to make the required graphs. You might want to store all this data in arrays which you can then dump into a file later on.

Repeat all of the above for several different runs. Do not average over them as this will either result in loss of data for individual runs or the average will just be a straight line. Plot all of these runs on the same graph and/or choose one of the runs as a "typical" run and plot it.

Also repeat all of the above for several different combinations of the five parameters given at the beginning of this document. For each combination of parameters, do several runs as explained in the previous paragraph.

Example output

Here is an example of what the graphs from your experiments might look like. In this example, G=50, l=20, N=30, pm=0.033, and pc=0.6.

      

The first graph shows both the average and best fitness for 6 different runs at each generation. The top set of curves are the best fitness values for the 6 runs, and the lower set of curves are the average fitness values for these 6 runs.

The second graph shows the number of correct bits of the best individual (based on fitness) for each of the 6 runs at each generation.



Grad portion

Learning

Between steps 5 and 6 above (i.e., before updating the population in step 6), perform learning on every other bit of each offspring string as described below.

Iterate through each of the new offspring and for each offspring, iterate for 20 guesses, doing the following:

a. You will want to work with a copy of each offspring string rather than directly manipulate the offspring string. This is because you want to try 20 different manipulations of the original offspring string, so you need to keep that original string intact.

b. For every other bit in the copy of the offspring, randomly assign that bit to be a 0 or a 1.

c. Calculate the fitness of the manipulated copy of the offspring by using the fitness function F(s)=(x/2l)10.

d. If this fitness is greater than the max fitness found in the previous guesses of this offspring, then update the max fitness and also copy the manipulated offspring string back into that offspring string.

Note: Even though 10 of the bits will be determined by learning, it is OK if you perform crossover and mutation on all the bits in steps 4 and 5 above. Then when the code executes the learning part, 10 of the bits will just be overwritten by the learning mechanism, so it doesn't matter what those bits were before learning.

Sudden change in environment

In your code, after you've gone through steps 2–7 with a fixed number of generations (G*), record the average fitness and best fitness at generation G*.

While the average and best fitness are less than the recorded average and best fitness, repeat steps 2-7 above, this time using the fitness function F2(s)=(1–x/2l)10.

Keep a counter indicating how many generations are required for the population to recover to the fitness levels achieved before the environmental change.

Do the above in both non-learning and learning modes.

Note: It is possible that your program might hang while running this test. This is caused by not being able to find two distinct parents in step 3, and so the program takes "forever" looking for them. If this happens, then modify the part of your code that selects parents and if the program is unable to find two distinct parents within a certain number of tries, then just let a parent mate with itself. Since this is just a simple "toy" problem, this really is not much of an issue.



Possible additional work (for all students)

Here are several additional things you can try so as to earn a higher grade on the project.

  1. The more parameter combinations you try, the better (within reason, of course).
  2. Try different mutation mechanisms.
  3. Try different crossover mechanisms.
  4. Try different fitness functions.
  5. For the graduate portion, try a few different sets of parameters in addition to the one suggested.
  6. For the graduate portion, try different numbers of guesses in the learning part.
  7. As always, more in-depth analysis and observations will lead to higher grades.
  8. Undergrads can do any of the graduate portion work.
  9. Anything else that you can come up with.