Lab 8: Genetic Algorithms
Due November 17 by midnight

strand of DNA

Starting point code

Use Teammaker to form your team. You can log in to that site to indicate your partner preference. Once you and your partner have specified each other, a GitHub repository will be created for your team.

Introduction

The objectives of this lab are to:

The main steps of this lab are:

You will modify the following files:

You will use, but not modify, our implementation of neural networks from Lab 5, which is called neural_net.py. The Network class has been updated with two additional methods getWeights() and setWeights() to facilitate using neural networks within a genetic algorithm.

Implement the GeneticAlgorithm class

Open the file ga.py. You have been given a skeleton definition of the GeneticAlgorithm class that you will complete. Read through the methods that you will need to implement. Notice that the last two methods, called fitness and isDone, will not be implemented here. Instead they must be inherited and overridden.

Open up the file sum_ga.py. This contains an example class that overrides these methods. As you implement methods in the GeneticAlgorithm class, you will call them in the main program of this file to ensure that they are working as expected. Build your implementation incrementally, testing each new set of methods as you go.

  1. Start by implementing the methods initializePopulation and evaluatePopulation.

    To test them, modify the main program in the file sum_ga.py. Create an instance of the SumGA class where the length of the candidate solutions is 10 and the population size is 20. Use this instance to call the methods that you implemented. Next, add a for loop to print out each member of the population, and its fitness. Finally, print out the class variables you created to track the best ever fitness and best ever candidate solution. Ensure that they have been set correctly based on your initial population. To execute your test code do:

        python3 sum_ga.py
      

  2. Now implement the three main operators used in a genetic algorithm: selection, crossover, and mutation. Below is a review of how each of these operators function.

  3. Implement the method oneGeneration that replaces the current population with a new population generated by applying the three operators: selection, crossover, and mutation. Pseudocode is shown below:
    initialize the newPopulation to an empty list
    loop until the size of newPopulation >= the desired popSize
       select two parents
       apply crossover to these parents to generate two children
       mutate both children
       add both children to the newPopulation
    if size of newPopulation is > desired popSize
       slice out the last member of the newPopulation
    population = newPopulation
    

    Test this method and see if the average fitness of the next population is higher than the average fitness of the previous population.

  4. Implement the method evolve that repeatedly calls oneGeneration until the maximum number of generations has been reached or the isDone method returns True. Pseudocode is shown below:
    set class variables given in parameters
    initialize a random population
    evaluate the population
    loop over max generations
       run one generation
       evaluate the population
       if done
          break
    return the best ever individual
    

  5. If your genetic algorithm is working properly, then the following main program for the file sum_ga.py should yield a graph similar to the one shown below.
     def main():
        # Chromosomes of length 20, population of size 50
        ga = SumGA(20, 50)
        # Evolve for 100 generations
        # High prob of crossover, low prob of mutation
        bestFound = ga.evolve(100, 0.6, 0.01)
        print(bestFound)
        ga.plotStats("Sum GA")
    
  6. sum GA results

    Once you are convinced that the genetic algorithm is working properly, you will then move on to creating a specialized genetic algorithm that can evolve the weights and biases of a neural network.

    Implement the NeuralGA class

    Open the file neural_ga.py. This class is a specialization of the basic GeneticAlgorithm class. Rather than creating chromosomes of bits, it creates chromosomes of neural network weights (floating point numbers). There are only two methods you need to re-implement: initalizePopulation and mutate.

    There is some test code provided that creates a population of size 5 of a small neural network with an input layer of size 2, an output layer of size 1, and no hidden layers. To execute this test code do:

        python3 neural_ga.py
      
    You should see that each chromosome contains small random weights and that about 30 percent of the weights get mutated to new small random weights.

    Once this is working, you are ready apply your NeuralGA class to a reinforcement learnng problem.

    Implement the RLNeuralGA class

    In Lab 7, we used approximate Q-learning to solve the Cart Pole problem. We accomplished this by approximating the Q-learning table with a neural network. The inputs to the neural network were the states of the problem and the outputs were the estimated Q-values of doing each action in the given state. We applied back-propagataion to learn the weights, using the Q-learning update rule to generate targets for training.

    Now that you have implemented GAs, we can tackle reinforcement learning problems in a new way. We will again use a neural network to represent the solution. Like before, the inputs to the network will represent the states of the problem, but now the outputs will represent the actions directly. Our goal will be to find a set of weights for the network such that the total reward received when following its action outputs is maximized. We will accomplish this by generating a population of random weight settings, and go through generations of selection, crossover, and mutation to find better and better weights. The fitness function will be the total reward received over an episode of choosing actions using the neural network.

    The benefits of this approach over Q-learing are that now our actions need not be discrete, and our policy for behavior is much more direct--we are not learning values first, and then building a policy from them. We will compare the GA to Q-learning at solving the Cart Pole problem.

    Open the file cartpole_neural_ga.py and complete the methods fitness and isDone. Here is pseudocode for the fitness method:

      create a neural network with the appropriate layer sizes
      set the weights of the network using the chromosome
      state, _ = reset the env
      initialize total_reward to 0
      loop over steps 
         when render is True, render the env
         get the network's output from predicting the current state
         choose action with max output value (hint: use np.argmax)
         step the environment using that action saving the result
         get the state, reward, and done from the result
         update total_reward using the current reward
         when done, break from the loop
      return total_reward
    

    In order to use OpenAI Gym you'll need to activate the CS63 virtual environment:

    source /usr/swat/bin/CS63env
    

    To run the code do:

    python3 cartpole_neural_ga.py
    

    You should find that the GA can fairly quickly discover a set of weights that solve the Cart Pole problem.

    cart pole GA results

    The cart pole GA will save the neural network weights associated with the best ever chromosome found in a file called bestEver.wts. To test out these weights do:

          python3 testBest.py  
          
    This will render the cart pole environment and you should see that the pole is balanced for all 200 steps. There is some randomness in the testing, so some tests may not make it all the way until the end.

    Submitting

    Use git to add, commit, and push the files that you modified.