Evolution

Edit on Github


Neuro-evolution is something that is fairly underused in the machine learning community. It is quite interesting to see new architectures develop for complicated problems. In this guide I will tell you how to set up a simple supervised neuro-evolution process. If you want to do an unsupervised neuro-evolution process, head over to the NEAT page.

The training set

You always have to supply a training set for supervised learning. First of all, you should normalize your data. That means you have to convert all your input and output data to values within a range of 0 to 1. If you are unsure how to do this, visit the Normalization tutorial. Each training sample in your training set should be an object that looks as follows:

{ input: [], output: [] }

So an example of a training set would be (XOR):

var myTrainingSet = [
  { input: [0,0], output: [0] },
  { input: [0,1], output: [1] },
  { input: [1,0], output: [1] },
  { input: [1,1], output: [0] }
];

The network architecture

You can start off with any architecture. You can even use the evolution process to optimize already trained networks. However, I advise you to start with an empty network, as originally described in the NEAT paper. The constructor of an empty network is as follows:

var myNetwork = new Network(inputSize, outputSize);

So for the training set I made above, the network can be constructed as follows:

var myNetwork = new Network(2, 1); // 2 inputs, 1 output

Now we have our network. We don't have to tinker around anymore; the evolution process will do that for us.

Evolving the network

Be warned: there are a lot of options involved in the evolution of a process. It's a matter of trial and error before you reach options that work really well. Please note that most options are optional, as default values have been configured.

Please note that the evolve function is an async function, so you need to wrap it in an async function to call await.

The evolve function is as follows:

yourNetwork.evolve(yourData, yourOptions);

Check out the evolution options here and here. I'm going to use the following options to evolve the network:

  • mutation: methods.mutation.FFW - I want to solve a feed forward problem, so I supply all feed forward mutation methods. More info here.
  • equal: true - During the crossover process, parent networks will be equal. This allows the spawning of new network architectures more easily.
  • popsize: 100 - The default population size is 50, but 100 worked better for me.
  • elitism: 10 - I want to keep the fittest 10% of the population to the next generation without breeding them.
  • log: 10 - I want to log the status every 10 iterations.
  • error: 0.03 - I want the evolution process when the error is below 0.03;
  • iterations: 1000 - I want the evolution process to stop after 1000 iterations if the target error hasn't been reached yet.
  • mutationRate: 0.5 - I want to increase the mutation rate to surpass possible local optima.

So let's put it all together:

await myNetwork.evolve(myTrainingSet, {
  mutation: methods.mutation.FFW,
  equal: true,
  popsize: 100,
  elitism: 10,
  log: 10,
  error: 0.03,
  iterations: 1000,
  mutationRate: 0.5
});

// results: {error: 0.0009000000000000001, generations: 255, time: 1078}
// please note that there is a hard local optima that has to be beaten

Now let us check if it actually works:

myNetwork.activate([0,0]); // [0]
myNetwork.activate([0,1]); // [1]
myNetwork.activate([1,0]); // [1]
myNetwork.activate([1,1]); // [0]

Notice how precise the results are. That is because the evolution process makes full use of the diverse activation functions. It actually uses the Activation.STEP function to get a binary 0 and 1 output.

Help

If you need more help, feel free to create an issue here!