Overview of the basic XOR example (xor2.py)¶
The xor2.py example, shown in its entirety at the bottom of this page, evolves a network that implements the two-input XOR function:
Input 1 | Input 2 | Output |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
Fitness function¶
The key thing you need to figure out for a given problem is how to measure the fitness of the genomes that are produced
by NEAT. Fitness is expected to be a Python float
value. If genome A solves your problem more successfully than genome B,
then the fitness value of A should be greater than the value of B. The absolute magnitude and signs of these fitnesses
are not important, only their relative values.
In this example, we create a feed-forward neural network based on the genome, and then for each case in the table above, we provide that network with the inputs, and compute the network’s output. The error for each genome is \(1 - \sum_i (e_i - a_i)^2\) between the expected (\(e_i\)) and actual (\(a_i\)) outputs, so that if the network produces exactly the expected output, its fitness is 1, otherwise it is a value less than 1, with the fitness value decreasing the more incorrect the network responses are.
This fitness computation is implemented in the eval_genomes
function. This function takes two arguments: a list
of genomes (the current population) and the active configuration. neat-python expects the fitness function to calculate
a fitness for each genome and assign this value to the genome’s fitness
member.
Running NEAT¶
Once you have implemented a fitness function, you mostly just need some additional boilerplate code that carries out the following steps:
- Create a
neat.config.Config
object from the configuration file (described in the Configuration file description). - Create a
neat.population.Population
object using theConfig
object created above. - Call the
run
method on thePopulation
object, giving it your fitness function and (optionally) the maximum number of generations you want NEAT to run.
After these three things are completed, NEAT will run until either you reach the specified number of generations, or at least one genome achieves the fitness_threshold value you specified in your config file.
Getting the results¶
Once the call to the population object’s run
method has returned, you can query the statistics
member of the
population (a neat.statistics.StatisticsReporter
object) to get the best genome(s) seen during the run.
In this example, we take the ‘winner’ genome to be that returned by pop.statistics.best_genome()
.
Other information available from the default statistics object includes per-generation mean fitness, per-generation standard deviation of fitness, and the best N genomes (with or without duplicates).
Visualizations¶
Functions are available in the visualize module to plot the best and average fitness vs. generation, plot the change in species vs. generation, and to show the structure of a network described by a genome.
Example Source¶
NOTE: This page shows the source and configuration file for the current version of neat-python available on GitHub. If you are using the version 0.92 installed from PyPI, make sure you get the script and config file from the archived source for that release.
Here’s the entire example:
"""
2-input XOR example -- this is most likely the simplest possible example.
"""
from __future__ import print_function
import os
import neat
import visualize
# 2-input XOR inputs and expected outputs.
xor_inputs = [(0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (1.0, 1.0)]
xor_outputs = [ (0.0,), (1.0,), (1.0,), (0.0,)]
def eval_genomes(genomes, config):
for genome_id, genome in genomes:
genome.fitness = 4.0
net = neat.nn.FeedForwardNetwork.create(genome, config)
for xi, xo in zip(xor_inputs, xor_outputs):
output = net.activate(xi)
genome.fitness -= (output[0] - xo[0]) ** 2
def run(config_file):
# Load configuration.
config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction,
neat.DefaultSpeciesSet, neat.DefaultStagnation,
config_file)
# Create the population, which is the top-level object for a NEAT run.
p = neat.Population(config)
# Add a stdout reporter to show progress in the terminal.
p.add_reporter(neat.StdOutReporter(True))
stats = neat.StatisticsReporter()
p.add_reporter(stats)
p.add_reporter(neat.Checkpointer(5))
# Run for up to 300 generations.
winner = p.run(eval_genomes, 300)
# Display the winning genome.
print('\nBest genome:\n{!s}'.format(winner))
# Show output of the most fit genome against training data.
print('\nOutput:')
winner_net = neat.nn.FeedForwardNetwork.create(winner, config)
for xi, xo in zip(xor_inputs, xor_outputs):
output = winner_net.activate(xi)
print("input {!r}, expected output {!r}, got {!r}".format(xi, xo, output))
node_names = {-1:'A', -2: 'B', 0:'A XOR B'}
visualize.draw_net(config, winner, True, node_names=node_names)
visualize.plot_stats(stats, ylog=False, view=True)
visualize.plot_species(stats, view=True)
p = neat.Checkpointer.restore_checkpoint('neat-checkpoint-4')
p.run(eval_genomes, 10)
if __name__ == '__main__':
# Determine path to configuration file. This path manipulation is
# here so that the script will run successfully regardless of the
# current working directory.
local_dir = os.path.dirname(__file__)
config_path = os.path.join(local_dir, 'config-feedforward')
run(config_path)
and here is the associated config file:
#--- parameters for the XOR-2 experiment ---#
[NEAT]
fitness_criterion = max
fitness_threshold = 3.9
pop_size = 150
reset_on_extinction = False
[DefaultGenome]
# node activation options
activation_default = sigmoid
activation_mutate_rate = 0.0
activation_options = sigmoid
# node aggregation options
aggregation_default = sum
aggregation_mutate_rate = 0.0
aggregation_options = sum
# node bias options
bias_init_mean = 0.0
bias_init_stdev = 1.0
bias_max_value = 30.0
bias_min_value = -30.0
bias_mutate_power = 0.5
bias_mutate_rate = 0.7
bias_replace_rate = 0.1
# genome compatibility options
compatibility_disjoint_coefficient = 1.0
compatibility_weight_coefficient = 0.5
# connection add/remove rates
conn_add_prob = 0.5
conn_delete_prob = 0.5
# connection enable options
enabled_default = True
enabled_mutate_rate = 0.01
feed_forward = True
initial_connection = full
# node add/remove rates
node_add_prob = 0.2
node_delete_prob = 0.2
# network parameters
num_hidden = 0
num_inputs = 2
num_outputs = 1
# node response options
response_init_mean = 1.0
response_init_stdev = 0.0
response_max_value = 30.0
response_min_value = -30.0
response_mutate_power = 0.0
response_mutate_rate = 0.0
response_replace_rate = 0.0
# connection weight options
weight_init_mean = 0.0
weight_init_stdev = 1.0
weight_max_value = 30
weight_min_value = -30
weight_mutate_power = 0.5
weight_mutate_rate = 0.8
weight_replace_rate = 0.1
[DefaultSpeciesSet]
compatibility_threshold = 3.0
[DefaultStagnation]
species_fitness_func = max
max_stagnation = 20
species_elitism = 2
[DefaultReproduction]
elitism = 2
survival_threshold = 0.2