Simulating Evolution With "Jellies" and Neural Networks

In this post, I share some interesting results from a neuroevolution simulation. My goal here was to observe what happens when neural network powered “jellies” are left to fend for themselves as they compete to collect food pebbles and to avoid contact with each other.

Firstly, I put together a simple 3-layer neural network class for them to control their movements.

public float[] Think()
{
   // for each neuron in each layer, get the output of the previous layer,
   // run the transfer function, and run the activation function

   float[] InputLayerOutput = new float[InputLayer.Length];
   for (int i = 0; i < InputLayer.Length; i++)
   {
       InputLayerOutput[i] = InputLayer[i].Value;
   }

   float[] HiddenLayerOutput = new float[HiddenLayer.Length];
   for (int i = 0; i < HiddenLayer.Length; i++)
   {
      HiddenLayer[i].DendriteValues = InputLayerOutput;
      HiddenLayer[i].Activation(HiddenLayer[i].Transfer());
      HiddenLayerOutput[i] = HiddenLayer[i].Output;
   }

   float[] OutputLayerOutput = new float[OutputLayer.Length];
   for (int i = 0; i < OutputLayer.Length; i++)
   {
      OutputLayer[i].DendriteValues = HiddenLayerOutput;
      OutputLayer[i].Activation(HiddenLayer[i].Transfer());
      OutputLayerOutput[i] = OutputLayer[i].Output;
   }

   return OutputLayerOutput;
}

The jellies will be able to see their environment by shooting lasers out of their eyes (cool). I’ll use raycasting for this.

public Ray(PointF start, float angle)
{
    this.Start = start;
    this.Angle = angle;
    End = new PointF
    (
        (float)Math.Cos(Angle) * ViewDistance + Start.X, (float)Math.Sin(Angle) * ViewDistance + Start.Y
    );
}
// The eyes are an array of rays ready to send signals to the brain
Brain.InputLayer[i].Value = Eyes[i].Raycast();
With this raycasting system, the jellies can “see” their environment. We’ll use this as input for their neural networks.

With this raycasting system, the jellies can “see” their environment. We’ll use this as input for their neural networks.

We can also visualize the neural networks as the jellies move around.

We can also visualize the neural networks as the jellies move around.

C# is cool because you can create some very simple and readable high-level code with respectable performance. On every new frame, the following update function is called:

private void Update(Graphics graphics)
{
   foreach (Jelly Jelly in Jellies)
   {
      Jelly.See();
      float[] BrainOutput = Jelly.Brain.Think();
      Jelly.Act(BrainOutput);
      Jelly.Draw(graphics);
   }

   foreach (Pebble Pebble in Pebbles)
   {
      Pebble.Act();
      Pebble.Draw(graphics);
   }

   if (Tick >= GenerationTime)
   {
       Evolve();
   }
}

Let’s talk about the Evolve function. Only the best jellies are allowed to breed due to stochastic universal sampling, which chooses two pseudo-random optimal jellies for chromosomal crossover. the crossover function works by choosing a point in each chromosome and switching everything after that point with the other chromosome's values. This is called (surprise) "single-point crossover".

Jelly Mom = FitnessProportionateSelection();
Jelly Dad = FitnessProportionateSelection();

// Not safe for work
float[] SonChromosome = Crossover(Mom.Brain.Chromosome, Dad.Brain.Chromosome)[0];
float[] DaughterChromosome = Crossover(Mom.Brain.Chromosome, Dad.Brain.Chromosome)[1];

Here’s an excerpt from the Crossover function:

// Choose a point on the chromosome to crossover
int Crosspoint = God.RandInt(0, mom.Length - 1);
for (int i = 0; i < Crosspoint; i++)
{
    Son[i] = Dad[i];
    Daughter [i] = Mom[i];
}
for (int i = Crosspoint; i < mom.Length; i++)
{
    Son[i] = Dad[i];
    Daughter [i] = Mom[i];
}

// Add some random variation after the chromosomes are combined
Son = Mutate(Son);
Daughter = Mutate(Daughter);

Now that my oversimplified briefing of how the simulation works is over, we can watch them evolve.

Generation1.gif

On the 1st generation, they reacted to their environment with no rhyme or reason. After all, I didn't expect to see anything astounding; It was a group of randomly generated chromosomes.

Generation4and5.gif

On the 5th generation, we see that some learning has happened. The pink jellies are outperforming the others and evolution is making sure that they reproduce more.

Generation25.gif

On the 25th generation, the pink jellies have figured it all out. They don’t bump into each other and they have mastered the fine art of collecting pebbles

This is cool, but nothing too unexpected. Let’s play around and see what happens when we remove food pebbles from the equation!

Wow! They developed a herd instinct! By circling together like this, the entire colony can benefit and the jellies can better avoid contact with each other.

This result stunned me. It may be a simple simulation, but we can use it learn how real-world behaviours came to be. If you want to learn more and see some even cooler results, I highly recommend you check out Karl Sims’ research, or try building something like this on your own!

Previous
Previous

Forecasting Option Premiums with Deep Learning