from neat3 import config, population, chromosome, genome, visualize
from neat3.nn import nn_pure as nn
import pickle
In order to run NEAT, you'll need to configure a number of paramters. There is a configuration file, provided below, where these parameters are set.
The [phenotype] section defines the type of neural networks that will be evolved. You need to ensure that the number of input nodes and output nodes match the requirements of your problem domain. Typically you will start with zero hidden nodes, and let NEAT add them if necessary to achieve higher fitness. The nn_activation defines what activation function will be used. The function exp is a sigmoid and will only return values in the range [0,1]. The function tanh will return values in the range [-1,1]. Typically for robot control problems we will use tanh.
The [genetic] section defines the size of the population and the maximum fitness threshold. Both of these should be tuned for your specific problem domain. When deciding on a pop_size, keep in mind that the larger the population, the longer the evolution process will take. However, larger populations tend to yield better results. You'll need to decide the best tradeoff of size vs speed. The max_fitness_threshold is used by NEAT to decide whether to stop evolution early. If this threshold is ever achieved, NEAT will suspend evolution, otherwise it will continue until the specified number of generations is reached.
The other sections tune how NEAT operates. I tend to leave these at the default settings, but feel free to experiment with them.
%%file configXOR
[phenotype]
input_nodes = 2
output_nodes = 1
max_weight = 30
min_weight = -30
feedforward = 1
nn_activation = exp
hidden_nodes = 0
weight_stdev = 0.9
[genetic]
pop_size = 50
max_fitness_threshold = 1
# Human reasoning
prob_addconn = 0.1
prob_addnode = 0.05
prob_mutatebias = 0.2
bias_mutation_power = 0.5
prob_mutate_weight = 0.9
weight_mutation_power = 1.5
prob_togglelink = 0.01
elitism = 1
[genotype compatibility]
compatibility_threshold = 3.0
compatibility_change = 0.0
excess_coeficient = 1.0
disjoint_coeficient = 1.0
weight_coeficient = 0.4
[species]
species_size = 10
survival_threshold = 0.2
old_threshold = 30
youth_threshold = 10
old_penalty = 0.2
youth_boost = 1.2
max_stagnation = 15
Create a fitness function that takes a chromosome as input and as a side-effect sets the chromosome's fitness. It does not return anything. A fitness function should be defined such that higher values equate with better fitness.
For this task, we want to define fitness to match how well the network solves the XOR problem. We first calculate the sum-squared error of a network on the XOR task. Good networks will have low error, so we will define fitness as 1 - sqrt(avgError). In order to evaluate a NEAT chromosome, you must first convert the chromosome into a phenotype (a network). Then you can activate the network and test its output values.
import math
INPUTS = [[0, 0], [0, 1], [1, 0], [1, 1]]
OUTPUTS = [0, 1, 1, 0]
def eval_individual(chromo):
net = nn.create_ffphenotype(chromo)
error = 0.0
for i, inputs in enumerate(INPUTS):
net.flush()
output = net.sactivate(inputs)
error += (output[0] - OUTPUTS[i])**2
chromo.fitness = 1 - math.sqrt(error/len(OUTPUTS))
NEAT requires a population evaluation function that takes a population as input. A population is defined to be a list of chromosomes. This function will be called once for every generation of the evolution, and it will evaluate the fitness of every member of the population.
def eval_population(population):
for chromo in population:
eval_individual(chromo)
The evolve function takes the number of generations to run. Each generation, NEAT will print out statistics about the population, including the current best fitness and the average fitness.
def evolve(n):
config.load("configXOR")
chromosome.node_gene_type = genome.NodeGene
# Tell NEAT that we want to use the above function to evaluate fitness
population.Population.evaluate = eval_population
# Create a population (the size is defined in the configuration file)
pop = population.Population()
# Run NEAT's genetic algorithm for at most 30 epochs
# It will stop if fitness surpasses the max_fitness_threshold in config file
pop.epoch(n, report=True, save_best=True, name="XOR")
# Plots the evolution of the best/average fitness
visualize.plot_stats(pop.stats)
# Visualizes speciation
visualize.plot_species(pop.species_log)
evolve(30)
NEAT will save several plots, one showing the best and average fitness over time, and another showing how NEAT's population speciated over time. These plots will have .svg extensions and can be opened from a terminal window using eog. Try viewing these plots.
In addition, NEAT will save the best chromosome present in the population at the end of each generation. To test a particular chromosome file, you can use the following function. This will also generate a new .svg file that includes a visualization of the network's topology. Try viewing this plot which will have a filename that begins with phenotype.
def eval_best(chromo_file):
fp = open(chromo_file, "rb")
chromo = pickle.load(fp)
fp.close()
print(chromo)
visualize.draw_net(chromo, "_"+ chromo_file)
print('\nNetwork output:')
brain = nn.create_ffphenotype(chromo)
for i, inputs in enumerate(INPUTS):
output = brain.sactivate(inputs)
print("%1.5f \t %1.5f" %(OUTPUTS[i], output[0]))
eval_best("XOR_best_chromo_29")
Run 10 experiments on the XOR problem using a popultion of size 50. For each experiment, record whether NEAT evolved a successful solution, and the number of hidden nodes it generated.
Run 10 more experiments on the XOR problem using a different population size (you can try smaller or larger, whichever you prefer). Record the same information as before, and compare the results.
Save this notebook before moving on.