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 minus the root mean square difference between the expected and actual 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_fitness function. The single argument to this function is a list of genomes in the current population. 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 Configuration file format).
  • Create a neat.population.Population object using the Config object created above.
  • Call the epoch method on the Population object, giving it your fitness function and 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 max_fitness_threshold value you specified in your config file.

Getting the results

Once the call to the population object’s epoch method has returned, a list of the most fit genome for each generation is available as the most_fit_genomes member of the population. We take the ‘winner’ genome as the last genome in this list.

A list of the average fitness for each generation is also available as avg_fitness_scores.

Visualizations

Functions are available in the neat.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.6 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 """
from __future__ import print_function

from neat import nn, population, statistics, visualize

# Network inputs and expected outputs.
xor_inputs = [[0, 0], [0, 1], [1, 0], [1, 1]]
xor_outputs = [0, 1, 1, 0]


def eval_fitness(genomes):
    for g in genomes:
        net = nn.create_feed_forward_phenotype(g)

        sum_square_error = 0.0
        for inputs, expected in zip(xor_inputs, xor_outputs):
            # Serial activation propagates the inputs through the entire network.
            output = net.serial_activate(inputs)
            sum_square_error += (output[0] - expected) ** 2

        # When the output matches expected for all inputs, fitness will reach
        # its maximum value of 1.0.
        g.fitness = 1 - sum_square_error


pop = population.Population('xor2_config')
pop.run(eval_fitness, 300)

print('Number of evaluations: {0}'.format(pop.total_evaluations))

# Display the most fit genome.
winner = pop.statistics.best_genome()
print('\nBest genome:\n{!s}'.format(winner))

# Verify network output against training data.
print('\nOutput:')
winner_net = nn.create_feed_forward_phenotype(winner)
for inputs, expected in zip(xor_inputs, xor_outputs):
    output = winner_net.serial_activate(inputs)
    print("expected {0:1.5f} got {1:1.5f}".format(expected, output[0]))

# Visualize the winner network and plot/log statistics.
visualize.plot_stats(pop.statistics)
visualize.plot_species(pop.statistics)
visualize.draw_net(winner, view=True, filename="xor2-all.gv")
visualize.draw_net(winner, view=True, filename="xor2-enabled.gv", show_disabled=False)
visualize.draw_net(winner, view=True, filename="xor2-enabled-pruned.gv", show_disabled=False, prune_unused=True)
statistics.save_stats(pop.statistics)
statistics.save_species_count(pop.statistics)
statistics.save_species_fitness(pop.statistics)

and here is the associated config file:

#--- parameters for the XOR-2 experiment ---#

# The `Types` section specifies which classes should be used for various
# tasks in the NEAT algorithm.  If you use a non-default class here, you
# must register it with your Config instance before loading the config file.
[Types]
stagnation_type      = DefaultStagnation
reproduction_type    = DefaultReproduction

[phenotype]
input_nodes          = 2
hidden_nodes         = 0
output_nodes         = 1
initial_connection   = unconnected
max_weight           = 30
min_weight           = -30
feedforward          = 1
activation_functions = sigmoid
weight_stdev         = 1.0


[genetic]
pop_size                = 150
max_fitness_threshold   = 0.95
prob_add_conn           = 0.988
prob_add_node           = 0.085
prob_delete_conn        = 0.146
prob_delete_node        = 0.0352
prob_mutate_bias        = 0.0509
bias_mutation_power     = 2.093
prob_mutate_response    = 0.1
response_mutation_power = 0.1
prob_mutate_weight      = 0.460
prob_replace_weight     = 0.0245
weight_mutation_power   = 0.825
prob_mutate_activation  = 0.0
prob_toggle_link        = 0.0138
reset_on_extinction     = 1

[genotype compatibility]
compatibility_threshold = 3.0
excess_coefficient      = 1.0
disjoint_coefficient    = 1.0
weight_coefficient      = 0.4

[DefaultStagnation]
species_fitness_func = mean
max_stagnation       = 15

[DefaultReproduction]
elitism              = 1
survival_threshold   = 0.2