# CS81 Lab4: Novelty Search

Due by noon next Friday

In this lab you will implement novelty search, introduced by Joel Lehman and Kenneth O. Stanley, and combine it with NEAT to conduct an evolutionary search based on a novelty metric rather than an objective fitness function. Lehman and Stanley argue that objective fitness functions can be deceptive, leading the evolutionary search to local maxima rather than towards the goal. In contrast, novelty search ignores the objective and simply searches for novel behavior, and therefore cannot be deceived.

Do an update81 to ge the starting point files for this lab.

1. Create a class to implement novelty search

Before trying to implement novelty search, review the details of the algorithm on pages 12-14 of the paper Abandoning objectives: Evolution through the search for novelty alone.

In the file noveltySearch.py, create a python class called Novelty. Implement the following methods:

• __init__(self, k, threshold, limit)
Novelty search stores an archive of examples from the behavior space. It bases the novelty metric on the k-nearest neighbors from that archive. Create class variables to store the archive as an initially empty list, the k to be used in calculating nearest neighbors, the threshold for how novel an example has to be before it will be added the archive, and the limit for the maximum size of the archive.
• distance(self, p1, p2)
Returns the distance between points p1 and p2 which are assumed to be lists or tuples of equal length. To calculate distance, sum up the squared differences between each component of the points. Return the square root of this sum. This method should work for points of any length.
• distFromkNearest(self, p)
Returns the distance of a point p from its k-nearest neighbors in the archive. The simplest, though very inefficient, way to implement this is to calculate the distance of p from every point in the archive, sort these distances, and then sum up and return the first k (which will be the closest).
• sparseness(self, p)
Returns the sparseness of the given point p as defined by equation 1 on page 13 of the paper. Recall that sparseness is a measure of how unique this point is relative to the archive of saved examples. Use the method distFromkNearest as a helper in calculating this value.
If the size of the archive is less than the limit, then always add the point p. Otherwise, when the archive is full, check if the given sparseness of point p is greater than the threshold. If so, add this point and also remove the oldest point in the archive.
• saveArchive(self, filename)
This method saves the entire archive to the given filename. Write each point on a single line in the file, with spaces separating each value.

2. Test methods of novelty search on random points in 2d
• In the main program at the bottom of the file noveltySearch, create an instance of your Novelty class with a k of 10, a threshold of 0.3, and a limit of 100.
• Use a for loop to generate 1000 random 2d points, where each value is in the range [0.0, 0.3], and add them to the archive.
• Use another for loop to generate 1000 random 2d points, where each value is in the range [0.0, 1.0], and add them to the archive.
• After both loops, save the archive to a file called test.dat.
• At the end of the main program print the sparseness of several points:
• Point (0.5, 0.5) should have a low sparseness since it is at the center of the range of random points you generated.
• Point (1, 1) should have a higher sparseness since it is at the boundary of random points you generated.
• Point (2, 2) should have an even higher sparseness since it is outside the range of random points you generated.
• Point (5, 5) should have the highest sparseness since it is far outside the range of random points you generated.
• Run your program and ensure that the sparseness values match these expectations.
• Graph the points that were saved from the archive by doing:
```% xgraph -P -nl test.dat
```
The P flag makes each point large, and the nl flag indicates that there should be no lines drawn between the points.
• Run your program a number of times, changing the threshold of the novelty search.
• Use 0.0 to allow any point to be added to the archive.
• Use 0.2 to only moderately test the novelty of points that are added.
• Use 0.3 to have a more stringest level of novelty for points.
• Finally try 0.4.
Look at the graph of the test.dat file after each run. How does the graph change?
• Run your program a number of times, this time keeping the threshold fixed at 0.3, while varying k. Again look at the graph at the end. How does changing k affect the results?

3. Test novelty search on XOR
• Open the file neatXOR.py. This is almost identical to the file evolveXOR.py from last week. The main difference is that the eval_fitness function includes an if at the end to check for when a solution has been found. It then sets the fitness for that chromosome to a high enough value to trigger the max_fitness_threshold ending the evolution. It also saves the network that solved XOR in a file called neat_chromo.
• We will compare standard NEAT to novelty search combined with NEAT for solving XOR. Let's test standard NEAT first. Run the program neatXOR.py 10 times and save the results in the file experimentsXOR. Record the generation in which a solution was found or 70 (the maximum number of generations) if not found. Also record the complexity of the solution, which is printed in the form: (number of hidden nodes, number of connections), at the end of each run. You can use the program evaluateXOR.py to test any of the neat_chromo solution files. You can use xv to look at any of the avg_fitness.svg plots that are produced. Notice that when using standard NEAT both the average and best fitness tend to increase over the course of evolution.
• Copy the file neatXOR.py to a file called noveltyXOR.py. Edit the noveltyXOR.py file as follows:
• Import your novelty search code.
• Create an instance of your Novelty class that uses a k of 10, a threshold of 0.3, and a limit of 150.
• Re-write the function eval_fitness so that it produces a novelty metric rather than an objective measure. In order to do this we need to create a representation of the behavior. For XOR this will be a 4d point containing the output produced by the network on each of the four inputs. For each chromosome in the population, create this point, calculate its sparseness, add it to the archive of novelty search, and then set the fitness of the chromosome to the spareness. Continue to calculate the error and use it to determine when a solution has been found. Save the solution in a file called novelty_chromo.
• After the call to pop.epoch(...), save the archive from novelty search into a file called archive.dat.
• Before doing experiments with the novelty version, test it to see if it is giving reasonable results. It should find a solution most of the time. Use the program evaluateXOR.py to test a few solutions. Also look at the file archive.dat to see if it contains a reasonable variety of example behavior. Once you are conviced it is working, you are reading to run experiments.
• Run the program noveltyXOR.py 10 times and record the same information as for the previous experiments. Look at some of the solutions produced as well as the plots of average and best fitness. Notice that in the fitness plots, novelty search does not display the same trend of increasing fitness values over time.
• At the bottom of the file experimentsXOR compare the results. Which method finds solutions more consistently? Which method finds solutions more quickly? Which method finds less complex solutions? Is the XOR problem better suited for objective-based or novelty-based search, why?

Optional: Test novelty search on maze traversal

Before trying to implement maze traversal, read through the details of the maze experiment that Lehman and Stanley conducted on pages 14-16 of their paper Abandoning objectives: Evolution through the search for novelty alone.

• They used a robot with 6 range sensors for detecting obstacles and 4 radar sensors for detecting the goal location. Rather than trying to replicate this experiment exactly we can use the existing framework of pyrobot to create a similar experiment. The goal location can be marked with a light. The robot can be equipped with sonar sensors to detect obstacles and light sensors to detect the goal.
• There is a series of maze-like worlds that already exist in pyrobot called: LightBehindWall, LightInCorral, LightBehindCorral, LightEnclosed, and LightInMaze that could be used as a testbed for maze traversal. These are located in /usr/local/pyrobot/plugins/worlds/Pyrobot/. You are also welcome to create your own maze worlds.
• Using the files evolveEnergy.py and evaluateEnergy.py from last week's lab as models, create files called neatMaze.py and evaluateMaze.py. In the file neatMaze.py create a standard objective fitness function based on the formula given at the bottom of page 15. Remember that you can use the pyrobot commands setPos to position the robot at the start of a trial and getPos to find the robot's position at the end of the trial. Look at the parameter settings in the appendix of the paper to determine how to configure NEAT. Test this program. Based on the results presented in the paper, we would expect that this would only very rarely solve a maze.
• Copy the file neatMaze.py to a file called noveltyMaze.py. Modify the fitness function so that it is now a novelty metric based on the robot's end point at the end of a trial. Look at the appendix of the paper to determine how to configure novelty search. Test this program. Based on the results presented in the paper, we would expect that this would find a solution to the maze almost every time.

Submit

When you are done run handin81 to turn in your completed lab work.