From 31087a5a2c5635a2de6b9c8d598ece7fd322bfbf Mon Sep 17 00:00:00 2001 From: shiffman Date: Wed, 16 Aug 2023 21:23:35 +0000 Subject: [PATCH] Notion - Update docs --- content/10_nn.html | 213 +++++++++++++++++- .../10_nn/creature_sensors/creature.js | 39 ++++ .../examples/10_nn/creature_sensors/food.js | 12 + .../10_nn/creature_sensors/index.html | 16 ++ .../examples/10_nn/creature_sensors/sensor.js | 20 ++ .../examples/10_nn/creature_sensors/sketch.js | 17 ++ .../examples/10_nn/creature_sensors/style.css | 8 + .../neuro_evolution_steering_seek/creature.js | 80 +++++++ .../neuro_evolution_steering_seek/glow.js | 22 ++ .../neuro_evolution_steering_seek/index.html | 15 ++ .../population.js | 91 ++++++++ .../neuro_evolution_steering_seek/sketch.js | 80 +++++++ .../neuro_evolution_steering_seek/style.css | 8 + .../neuroevolution_ecosystem/creature.js | 116 ++++++++++ .../10_nn/neuroevolution_ecosystem/food.js | 12 + .../10_nn/neuroevolution_ecosystem/index.html | 16 ++ .../10_nn/neuroevolution_ecosystem/sensor.js | 20 ++ .../10_nn/neuroevolution_ecosystem/sketch.js | 39 ++++ .../10_nn/neuroevolution_ecosystem/style.css | 8 + 19 files changed, 830 insertions(+), 2 deletions(-) create mode 100644 content/examples/10_nn/creature_sensors/creature.js create mode 100644 content/examples/10_nn/creature_sensors/food.js create mode 100644 content/examples/10_nn/creature_sensors/index.html create mode 100644 content/examples/10_nn/creature_sensors/sensor.js create mode 100644 content/examples/10_nn/creature_sensors/sketch.js create mode 100644 content/examples/10_nn/creature_sensors/style.css create mode 100644 content/examples/10_nn/neuro_evolution_steering_seek/creature.js create mode 100644 content/examples/10_nn/neuro_evolution_steering_seek/glow.js create mode 100644 content/examples/10_nn/neuro_evolution_steering_seek/index.html create mode 100644 content/examples/10_nn/neuro_evolution_steering_seek/population.js create mode 100644 content/examples/10_nn/neuro_evolution_steering_seek/sketch.js create mode 100644 content/examples/10_nn/neuro_evolution_steering_seek/style.css create mode 100644 content/examples/10_nn/neuroevolution_ecosystem/creature.js create mode 100644 content/examples/10_nn/neuroevolution_ecosystem/food.js create mode 100644 content/examples/10_nn/neuroevolution_ecosystem/index.html create mode 100644 content/examples/10_nn/neuroevolution_ecosystem/sensor.js create mode 100644 content/examples/10_nn/neuroevolution_ecosystem/sketch.js create mode 100644 content/examples/10_nn/neuroevolution_ecosystem/style.css diff --git a/content/10_nn.html b/content/10_nn.html index 6034bc5..e26d2b4 100644 --- a/content/10_nn.html +++ b/content/10_nn.html @@ -1314,7 +1314,7 @@ function draw() {

Example 10.6: Neuroevolution Steering

-
+
@@ -1345,8 +1345,217 @@ function draw() { }

Neuroevolution Ecosystem

+

If I’m being honest here, this chapter is getting kind of long. My goodness, this book is incredibly long, are you really still here reading? I’ve been working on it for over ten years and right now, at this very moment as I type these letters, I feel like stopping. But I cannot. I will not. There is one more thing I must demonstrate, that I am obligated to, that I won’t be able to tolerate skipping. So bear with me just a little longer. I hope it will be worth it.

+

There are two key elements of what I’ve demonstrated so far that don’t fit into my dream of the Ecosystem Project that has been the throughline of this book. The first I covered already in chapter 9 with the introduction of the bloops. There is something very odd about a system of creatures that all lives and dies together, starting completely over with each subsequent generation. I’d like to examine that in the context of neuroevolution.

+

But even more so, there’s a major flaw in the way I am extracting features from a scene. The creatures in Example 10.6 are all knowing. They know precisely where the glow is regardless of how far away they are or what might be blocking their vision or senses. Yes, it may be reasonable to assume they are aware of their current velocity, but I didn’t introduce any limits to the perception of external elements in their environment.

+

A common approach in reinforcement learning simulations is to attach sensors to a given entity. For example, consider a simulated mouse in a maze searching for cheese in the dark. Its whiskers might act as proximity sensors to detect walls and turns. The mouse can’t see the entire maze, only is immediate surroundings. Another example is a bat using echolocation to navigate, or a car on a winding road that can only see what is projected in front of its headlights.

+

I’d like to build on this idea of the whiskers (or more formally “vibrissae”) found in mice, cats, and other mammals. In the real world, animals use their vibrissae to navigate and detect nearby objects, especially in the dark or obscured environments.

+

ILLUSTRATION OF A MOUSE OR CAT OR FICTIONAL CREATURE SENSING ITS ENVIRONMENT WITH ITS WHISKERS

+

My creatures will remain as simple circles, but will include whisker-like sensors that emanate from their center in all directions.

+
class Creature {
+  constructor(x, y) {
+    // The creature has a position and radius
+    this.position = createVector(x, y);
+    this.r = 16;
+    // The creature has an array of sensors
+    this.sensors = [];
+	
+    // The creature has a 5 sensors
+    let totalSensors = 5;
+    for (let i = 0; i < totalSensors; i++) {
+      // First, calculate a direction for the sensor 
+      let angle = map(i, 0, totalSensors, 0, TWO_PI);
+      // Create a vector a little bit longer than the radius as the sensor
+      this.sensors[i] = p5.Vector.fromAngle(angle).mult(this.r * 1.5);
+    }
+  }
+}
+

The code creates a series of vectors that each describe the direction and length of one whisker-like sensor attached to the creature. But just the vector is not enough. It would be nice for the sensor to be able to store a value, a numeric representation of what it is sensing. This value can be thought of as analogous to the intensity of touch. Just as a cat's whisker might detect a faint touch from a distant object or a stronger push from a closer one, the virtual sensor's value could range to represent proximity. Let’s assume there is a Food class to describe a circle of deliciousness that the creature is looking for.

+
class Food {
+  constructor() {
+    this.position = createVector(random(width), random(height));
+    this.r = 50;
+  }
+
+  show() {
+    noStroke();
+    fill(0, 100);
+    circle(this.position.x, this.position.y, this.r * 2);
+  }
+}
+

A Food object is a circle drawn according to a position and radius. I’ll assume the creature in my simulation has no vision and relies on sensors to detect if there is food nearby. This begs the question question: how can I determine if a sensor is touching the food? One approach is to use a technique called “raycasting.” This method is commonly employed in computer graphics to project rays (often representing light) from an origin point in a scene to determine what objects they intersect with. Raycasting is useful for visibility and collision checks, exactly what I am doing here!

+

Although raycasting is a robust solution, it requires more involved mathematics than I'd like to delve into here. For those interested, an explanation and implementation are available in Coding Challenge #145 on thecodingtrain.com. Instead, I will opt for a more straightforward approach and check whether the endpoint of a sensor lies inside the food circle.

+

[ILLUSTRATION OF SENSOR ENDPOINT CHECKING IF INSIDE CIRCLE]

+

As I want the sensor to store a value for its sensing along with the sensing algorithm itself, it makes sense to encapsulate these elements into a Sensor class.

+
class Sensor {
+  constructor(v) {
+    this.v = v.copy();
+    this.value = 0;
+  }
+  
+  sense(position, food) {
+    //{!1} Find the "tip" (or endpoint) of the sensor by adding position
+    let end = p5.Vector.add(position, this.v);
+    //{!1} How far is it from the food center
+    let d = end.dist(food.position);
+    //{!1} If it is within the radius light up the sensor  
+    if (d < food.r) {
+      // The further into the center the food, the more the sensor activates
+      this.value = map(d, 0, food.r, 1, 0);
+    } else {
+      this.value = 0;
+    }
+  }
+}
+

Notice how the sensing mechanism gauges how deep inside the food’s radius the endpoint is with the map() function. When the sensor's endpoint is just touching the outer boundary of the food, the value is starts at 0. As the endpoint moves closer to the center of the food, the value increases, maxing out at 1. If the sensor isn't touching the food at all, its value remains at 0. This gradient of sensory feedback mirrors the varying intensity of touch or pressure in the real world.

+

Let’s look at testing the sensors with one creature (controlled by the mouse) and one piece of food (placed at the center of the canvas). When the sensors touch the food, they light up and get brighter the closer to the center.

+
+

Example 10.7: Bloops with Sensors

+
+
+
+
+
+
let bloop, food;
+
+function setup() {
+  createCanvas(640, 240);
+  bloop = new Creature();
+  food = new Food();
+}
+
+function draw() {
+  background(255);
+
+  bloop.position.x = mouseX;
+  bloop.position.y = mouseY;
+
+  food.show();
+
+  bloop.sense(food);
+  bloop.show();
+}
+
+class Creature {
+  constructor(x, y) {
+    this.position = createVector(x, y);
+    this.r = 16;
+    this.sensors = [];
+
+    let totalSensors = 15;
+    for (let i = 0; i < totalSensors; i++) {
+      let a = map(i, 0, totalSensors, 0, TWO_PI);
+      let v = p5.Vector.fromAngle(a);
+      v.mult(this.r * 2);
+      this.sensors[i] = new Sensor(v);
+    }
+  }
+
+  sense(food) {
+    for (let i = 0; i < this.sensors.length; i++) {
+      this.sensors[i].sense(this.position, food);
+    }
+  }
+
+  //{inline} see book website for the drawing code
+}
+

Are you thinking what I’m thinking? Now what if the values of those sensors are the inputs to a neural network? Assuming I added all of the necessary physics properties to the Creature class, I could then write a new think() method that processes the sensor values through the neural network “brain” and outputs a steering force, just as with the previous two examples.

+
  think() {
+    // Build an input array from the sensor values
+    let inputs = [];
+    for (let i = 0; i < this.sensors.length; i++) {
+      inputs[i] = this.sensors[i].value;
+    }
+
+    // Predicting a steering force from the sensors
+    let outputs = this.brain.predictSync(inputs);
+    let angle = outputs[0].value * TWO_PI;
+    let magnitude = outputs[1].value;
+    let force = p5.Vector.fromAngle(angle).setMag(magnitude);
+    this.applyForce(force);
+  }
+

The logical next step would be to bring in all the usual pieces of the genetic algorithm, establishing a fitness function (how much food did each creature eat?) and performing selection after a fixed generation time period. But this is a great opportunity to test out the principles of a “bloop” ecosystem with a much more sophisticated environment and set of potential behaviors for the bloops themselves.

+

Instead of fixed lifespan cycle for the population of creatures, I will introduce the concept of health for each one. For every cycle through draw() that a bloop lives, the health deteriorates.

+
class Creature {  
+  constructor() {
+    //{inline} All of the creature's properties
+		
+    // The health starts at 100
+    this.health = 100;
+  } 
+
+  update() {
+    //{inline} the usual updating position, velocity, acceleration
+
+    // Losing some health!
+    this.health -= 0.25;
+  }
+

Now in draw(), if any bloop’s health drops below zero, it dies and is deleted from the array. And for reproduction, instead of performing the usual crossover and mutation all at once, each bloop (with a health grader than zero) will have a 0.1% chance of cloning itself.

+
  function draw() {
+    for (let i = bloops.length - 1; i >= 0; i--) {
+      if (bloops[i].health < 0) {
+        bloops.splice(i, 1);
+      } else if (random(1) < 0.001) {
+        let child = bloops[i].reproduce();
+        bloops.push(child);
+      }
+    }
+  }
+

Now, the bloops don’t just all die slowly over time. If there are able to consume food, their health increases. This will be managed in an eat() method of the Creature class.

+
  eat(food) {
+    let d = p5.Vector.dist(this.position, food.position);
+    if (d < this.r + food.r) {
+      this.health += 0.5;
+    }
+  }
+

And this is all that’s needed, a delicate balance of . . … . . . .. this final example includes a few additional features such as an array of food that shrinks as it gets eaten (re-spawning when it is depleted). Additionally, the bloops shrink as their health deteriorates.

+
+

Example 10.8: Neuroevolution Ecosystem

+
+
+
+
+
+
let bloops = [];
+let timeSlider;
+let food = [];
+
+function setup() {
+  createCanvas(640, 240);
+  ml5.tf.setBackend("cpu");
+  for (let i = 0; i < 20; i++) {
+    bloops[i] = new Creature(random(width), random(height));
+  }
+  for (let i = 0; i < 8; i++) {
+    food[i] = new Food();
+  }
+  timeSlider = createSlider(1, 20, 1);
+}
+
+function draw() {
+  background(255);
+  for (let i = 0; i < timeSlider.value(); i++) {
+    for (let i = bloops.length - 1; i >= 0; i--) {
+      bloops[i].think();
+      bloops[i].eat();
+      bloops[i].update();
+      bloops[i].borders();
+      if (bloops[i].health < 0) {
+        bloops.splice(i, 1);
+      } else if (random(1) < 0.001) {
+        let child = bloops[i].reproduce();
+        bloops.push(child);
+      }
+    }
+  }
+  for (let treat of food) {
+    treat.show();
+  }
+  for (let bloop of bloops) {
+    bloop.show();
+  }
+}

-

The end?

`The Ecosystem Project

Step 10 Exercise:

diff --git a/content/examples/10_nn/creature_sensors/creature.js b/content/examples/10_nn/creature_sensors/creature.js new file mode 100644 index 0000000..6467900 --- /dev/null +++ b/content/examples/10_nn/creature_sensors/creature.js @@ -0,0 +1,39 @@ +class Creature { + constructor(x, y) { + this.position = createVector(x, y); + this.r = 16; + this.sensors = []; + + let totalSensors = 15; + for (let i = 0; i < totalSensors; i++) { + let a = map(i, 0, totalSensors, 0, TWO_PI); + let v = p5.Vector.fromAngle(a); + v.mult(this.r * 2); + this.sensors[i] = new Sensor(v); + } + } + + sense(food) { + for (let i = 0; i < this.sensors.length; i++) { + this.sensors[i].sense(this.position, food); + } + } + + show() { + push(); + translate(this.position.x, this.position.y); + for (let sensor of this.sensors) { + stroke(0); + line(0, 0, sensor.v.x, sensor.v.y); + if (sensor.value > 0) { + fill(255, sensor.value*255); + stroke(0, 100) + circle(sensor.v.x, sensor.v.y, 8); + } + } + noStroke(); + fill(0); + circle(0, 0, this.r * 2); + pop(); + } +} diff --git a/content/examples/10_nn/creature_sensors/food.js b/content/examples/10_nn/creature_sensors/food.js new file mode 100644 index 0000000..9197e4c --- /dev/null +++ b/content/examples/10_nn/creature_sensors/food.js @@ -0,0 +1,12 @@ +class Food { + constructor() { + this.position = createVector(width / 2, height / 2); + this.r = 32; + } + + show() { + noStroke(); + fill(0, 100); + circle(this.position.x, this.position.y, this.r * 2); + } +} diff --git a/content/examples/10_nn/creature_sensors/index.html b/content/examples/10_nn/creature_sensors/index.html new file mode 100644 index 0000000..ca5e350 --- /dev/null +++ b/content/examples/10_nn/creature_sensors/index.html @@ -0,0 +1,16 @@ + + + + + + + Nature of Code Example 9.3: Smart Rockets + + + + + + + + + diff --git a/content/examples/10_nn/creature_sensors/sensor.js b/content/examples/10_nn/creature_sensors/sensor.js new file mode 100644 index 0000000..34ff8a4 --- /dev/null +++ b/content/examples/10_nn/creature_sensors/sensor.js @@ -0,0 +1,20 @@ +class Sensor { + constructor(v) { + this.v = v.copy(); + this.value = 0; + } + + sense(position, food) { + //{!1} Find the "tip" (or endpoint) of the sensor by adding position + let end = p5.Vector.add(position, this.v); + //{!1} How far is it from the food center + let d = end.dist(food.position); + //{!1} If it is within the radius light up the sensor + if (d < food.r) { + // The further into the center the food, the more the sensor activates + this.value = map(d, 0, food.r, 1, 0); + } else { + this.value = 0; + } + } +} \ No newline at end of file diff --git a/content/examples/10_nn/creature_sensors/sketch.js b/content/examples/10_nn/creature_sensors/sketch.js new file mode 100644 index 0000000..75b468d --- /dev/null +++ b/content/examples/10_nn/creature_sensors/sketch.js @@ -0,0 +1,17 @@ +let creature; +let food; + +function setup() { + createCanvas(640, 240); + creature = new Creature(); + food = new Food(); +} + +function draw() { + background(255); + creature.position.x = mouseX; + creature.position.y = mouseY; + food.show(); + creature.sense(food); + creature.show(); +} diff --git a/content/examples/10_nn/creature_sensors/style.css b/content/examples/10_nn/creature_sensors/style.css new file mode 100644 index 0000000..e78b710 --- /dev/null +++ b/content/examples/10_nn/creature_sensors/style.css @@ -0,0 +1,8 @@ +html, +body { + margin: 0; + padding: 0; +} +canvas { + display: block; +} diff --git a/content/examples/10_nn/neuro_evolution_steering_seek/creature.js b/content/examples/10_nn/neuro_evolution_steering_seek/creature.js new file mode 100644 index 0000000..992b4b0 --- /dev/null +++ b/content/examples/10_nn/neuro_evolution_steering_seek/creature.js @@ -0,0 +1,80 @@ +class Creature { + constructor(x, y, brain) { + this.position = createVector(x, y); + this.velocity = createVector(0, 0); + this.acceleration = createVector(0, 0); + this.r = 4; + this.maxspeed = 4; + this.fitness = 0; + + if (brain) { + this.brain = brain; + } else { + this.brain = ml5.neuralNetwork({ + inputs: 5, + outputs: 2, + task: "regression", + // neuroEvolution: true, + noTraining: true + }); + } + } + + seek(target) { + let v = p5.Vector.sub(target.position, this.position); + let distance = v.mag(); + v.normalize(); + let inputs = [ + v.x, + v.y, + distance / width, + this.velocity.x / this.maxspeed, + this.velocity.y / this.maxspeed, + ]; + + // Predicting the force to apply + let outputs = this.brain.predictSync(inputs); + let angle = outputs[0].value * TWO_PI; + let magnitude = outputs[1].value; + let force = p5.Vector.fromAngle(angle).setMag(magnitude); + this.applyForce(force); + } + + // Method to update location + update(target) { + // Update velocity + this.velocity.add(this.acceleration); + // Limit speed + this.velocity.limit(this.maxspeed); + this.position.add(this.velocity); + // Reset acceleration to 0 each cycle + this.acceleration.mult(0); + + let d = p5.Vector.dist(this.position, target.position); + if (d < this.r + target.r) { + this.fitness++; + } + } + + applyForce(force) { + // We could add mass here if we want A = F / M + this.acceleration.add(force); + } + + show() { + //{!1} Vehicle is a triangle pointing in the direction of velocity + let angle = this.velocity.heading(); + fill(127); + stroke(0); + strokeWeight(1); + push(); + translate(this.position.x, this.position.y); + rotate(angle); + beginShape(); + vertex(this.r * 2, 0); + vertex(-this.r * 2, -this.r); + vertex(-this.r * 2, this.r); + endShape(CLOSE); + pop(); + } +} diff --git a/content/examples/10_nn/neuro_evolution_steering_seek/glow.js b/content/examples/10_nn/neuro_evolution_steering_seek/glow.js new file mode 100644 index 0000000..924cdb7 --- /dev/null +++ b/content/examples/10_nn/neuro_evolution_steering_seek/glow.js @@ -0,0 +1,22 @@ +class Glow { + constructor() { + this.xoff = 0; + this.yoff = 1000; + this.position = createVector(); + this.r = 24; + } + + update() { + this.position.x = noise(this.xoff) * width; + this.position.y = noise(this.yoff) * height; + this.xoff += 0.01; + this.yoff += 0.01; + } + + show() { + stroke(0); + strokeWeight(2); + fill(200); + circle(this.position.x, this.position.y, this.r * 2); + } +} diff --git a/content/examples/10_nn/neuro_evolution_steering_seek/index.html b/content/examples/10_nn/neuro_evolution_steering_seek/index.html new file mode 100644 index 0000000..fd2ff64 --- /dev/null +++ b/content/examples/10_nn/neuro_evolution_steering_seek/index.html @@ -0,0 +1,15 @@ + + + + + + + Nature of Code Example 9.3: Smart Rockets + + + + + + + + diff --git a/content/examples/10_nn/neuro_evolution_steering_seek/population.js b/content/examples/10_nn/neuro_evolution_steering_seek/population.js new file mode 100644 index 0000000..c32d9cf --- /dev/null +++ b/content/examples/10_nn/neuro_evolution_steering_seek/population.js @@ -0,0 +1,91 @@ +// The Nature of Code +// Daniel Shiffman +// http://natureofcode.com + +// Pathfinding w/ Genetic Algorithms + +// A class to describe a population of "creatures" + +// Initialize the population +class Population { + constructor(mutation, length) { + this.mutationRate = mutation; // Mutation rate + this.population = new Array(length); // Array to hold the current population + this.generations = 0; // Number of generations + // Make a new set of creatures + for (let i = 0; i < this.population.length; i++) { + this.population[i] = new Rocket(320, 220); + } + } + + live(obstacles) { + // For every creature + for (let i = 0; i < this.population.length; i++) { + // If it finishes, mark it down as done! + this.population[i].checkTarget(); + this.population[i].run(obstacles); + } + } + + // Did anything finish? + targetReached() { + for (let i = 0; i < this.population.length; i++) { + if (this.population[i].hitTarget) return true; + } + return false; + } + + // Calculate fitness for each creature + calculateFitness() { + for (let i = 0; i < this.population.length; i++) { + this.population[i].calculateFitness(); + } + } + + selection() { + // Sum all of the fitness values + let totalFitness = 0; + for (let i = 0; i < this.population.length; i++) { + totalFitness += this.population[i].fitness; + } + // Divide by the total to normalize the fitness values + for (let i = 0; i < this.population.length; i++) { + this.population[i].fitness /= totalFitness; + } + } + + // Making the next generation + reproduction() { + let nextPopulation = []; + // Create the next population + for (let i = 0; i < this.population.length; i++) { + // Sping the wheel of fortune to pick two parents + let parentA = this.weightedSelection(); + let parentB = this.weightedSelection(); + let child = parentA.crossover(parentB); + // Mutate their genes + child.mutate(this.mutationRate); + nextPopulation[i] = new Rocket(320, 220, child); + } + // Replace the old population + this.population = nextPopulation; + this.generations++; + } + + weightedSelection() { + // Start with the first element + let index = 0; + // Pick a starting point + let start = random(1); + // At the finish line? + while (start > 0) { + // Move a distance according to fitness + start = start - this.population[index].fitness; + // Next element + index++; + } + // Undo moving to the next element since the finish has been reached + index--; + return this.population[index].brain; + } +} diff --git a/content/examples/10_nn/neuro_evolution_steering_seek/sketch.js b/content/examples/10_nn/neuro_evolution_steering_seek/sketch.js new file mode 100644 index 0000000..3053193 --- /dev/null +++ b/content/examples/10_nn/neuro_evolution_steering_seek/sketch.js @@ -0,0 +1,80 @@ +let creatures = []; +let timeSlider; +let lifeSpan = 250; // How long should each generation live +let lifeCounter = 0; // Timer for cycle of generation +let food; +let generations = 0; + +function setup() { + createCanvas(640, 240); + ml5.tf.setBackend("cpu"); + for (let i = 0; i < 50; i++) { + creatures[i] = new Creature(random(width), random(height)); + } + glow = new Glow(); + timeSlider = createSlider(1, 20, 1); + timeSlider.position(10, height - 20); +} + +function draw() { + background(255); + + glow.update(); + glow.show(); + + for (let creature of creatures) { + creature.show(); + } + + for (let i = 0; i < timeSlider.value(); i++) { + for (let creature of creatures) { + creature.seek(glow); + creature.update(glow); + } + lifeCounter++; + } + + if (lifeCounter > lifeSpan) { + normalizeFitness(); + reproduction(); + lifeCounter = 0; + generations++; + } + fill(0); + noStroke(); + text("Generation #: " + generations, 10, 18); + text("Cycles left: " + (lifeSpan - lifeCounter), 10, 36); +} + +function normalizeFitness() { + let sum = 0; + for (let creature of creatures) { + sum += creature.fitness; + } + for (let creature of creatures) { + creature.fitness = creature.fitness / sum; + } +} + +function reproduction() { + let nextCreatures = []; + for (let i = 0; i < creatures.length; i++) { + let parentA = weightedSelection(); + let parentB = weightedSelection(); + let child = parentA.crossover(parentB); + child.mutate(0.1); + nextCreatures[i] = new Creature(random(width), random(height), child); + } + creatures = nextCreatures; +} + +function weightedSelection() { + let index = 0; + let start = random(1); + while (start > 0) { + start = start - creatures[index].fitness; + index++; + } + index--; + return creatures[index].brain; +} diff --git a/content/examples/10_nn/neuro_evolution_steering_seek/style.css b/content/examples/10_nn/neuro_evolution_steering_seek/style.css new file mode 100644 index 0000000..e78b710 --- /dev/null +++ b/content/examples/10_nn/neuro_evolution_steering_seek/style.css @@ -0,0 +1,8 @@ +html, +body { + margin: 0; + padding: 0; +} +canvas { + display: block; +} diff --git a/content/examples/10_nn/neuroevolution_ecosystem/creature.js b/content/examples/10_nn/neuroevolution_ecosystem/creature.js new file mode 100644 index 0000000..ab9f822 --- /dev/null +++ b/content/examples/10_nn/neuroevolution_ecosystem/creature.js @@ -0,0 +1,116 @@ +class Creature { + constructor(x, y, brain) { + this.position = createVector(x, y); + this.velocity = createVector(0, 0); + this.acceleration = createVector(0, 0); + this.fullSize = 12; + this.r = this.fullSize; + this.maxspeed = 2; + this.sensors = []; + this.health = 100; + + let totalSensors = 15; + for (let i = 0; i < totalSensors; i++) { + let a = map(i, 0, totalSensors, 0, TWO_PI); + let v = p5.Vector.fromAngle(a); + v.mult(this.fullSize * 1.5); + this.sensors[i] = new Sensor(v); + } + + if (brain) { + this.brain = brain; + } else { + this.brain = ml5.neuralNetwork({ + inputs: this.sensors.length, + outputs: 2, + task: "regression", + noTraining: true, + // neuroEvolution: true, + }); + } + } + + reproduce() { + let brain = this.brain.copy(); + brain.mutate(0.1); + return new Creature(this.position.x, this.position.y, brain); + } + + eat() { + for (let i = 0; i < food.length; i++) { + let d = p5.Vector.dist(this.position, food[i].position); + if (d < this.r + food[i].r) { + this.health += 0.5; + food[i].r -= 0.05; + if (food[i].r < 20) { + food[i] = new Food(); + } + } + } + } + + think() { + for (let i = 0; i < this.sensors.length; i++) { + this.sensors[i].value = 0; + for (let j = 0; j < food.length; j++) { + this.sensors[i].sense(this.position, food[j]); + } + } + let inputs = []; + for (let i = 0; i < this.sensors.length; i++) { + inputs[i] = this.sensors[i].value; + } + + // Predicting the force to apply + const outputs = this.brain.predictSync(inputs); + let angle = outputs[0].value * TWO_PI; + let magnitude = outputs[1].value; + let force = p5.Vector.fromAngle(angle).setMag(magnitude); + this.applyForce(force); + } + + // Method to update location + update() { + // Update velocity + this.velocity.add(this.acceleration); + // Limit speed + this.velocity.limit(this.maxspeed); + this.position.add(this.velocity); + // Reset acceleration to 0 each cycle + this.acceleration.mult(0); + this.health -= 0.25; + } + + // Wraparound + borders() { + if (this.position.x < -this.r) this.position.x = width + this.r; + if (this.position.y < -this.r) this.position.y = height + this.r; + if (this.position.x > width + this.r) this.position.x = -this.r; + if (this.position.y > height + this.r) this.position.y = -this.r; + } + + applyForce(force) { + // We could add mass here if we want A = F / M + this.acceleration.add(force); + } + + show() { + push(); + translate(this.position.x, this.position.y); + for (let sensor of this.sensors) { + stroke(0, this.health * 2); + line(0, 0, sensor.v.x, sensor.v.y); + if (sensor.value > 0) { + fill(255, sensor.value * 255); + stroke(0, 100); + circle(sensor.v.x, sensor.v.y, 4); + } + } + noStroke(); + fill(0, this.health * 2); + this.r = map(this.health, 0, 100, 2, this.fullSize); + this.r = constrain(this.r, 2, this.fullSize); + circle(0, 0, this.r * 2); + pop(); + } +} diff --git a/content/examples/10_nn/neuroevolution_ecosystem/food.js b/content/examples/10_nn/neuroevolution_ecosystem/food.js new file mode 100644 index 0000000..ca0dc03 --- /dev/null +++ b/content/examples/10_nn/neuroevolution_ecosystem/food.js @@ -0,0 +1,12 @@ +class Food { + constructor() { + this.position = createVector(random(width), random(height)); + this.r = 50; + } + + show() { + noStroke(); + fill(0, 100); + circle(this.position.x, this.position.y, this.r * 2); + } +} diff --git a/content/examples/10_nn/neuroevolution_ecosystem/index.html b/content/examples/10_nn/neuroevolution_ecosystem/index.html new file mode 100644 index 0000000..ca5e350 --- /dev/null +++ b/content/examples/10_nn/neuroevolution_ecosystem/index.html @@ -0,0 +1,16 @@ + + + + + + + Nature of Code Example 9.3: Smart Rockets + + + + + + + + + diff --git a/content/examples/10_nn/neuroevolution_ecosystem/sensor.js b/content/examples/10_nn/neuroevolution_ecosystem/sensor.js new file mode 100644 index 0000000..9654ae4 --- /dev/null +++ b/content/examples/10_nn/neuroevolution_ecosystem/sensor.js @@ -0,0 +1,20 @@ +class Sensor { + constructor(v) { + this.v = v.copy(); + this.value = 0; + } + + sense(position, food) { + //{!1} Find the "tip" (or endpoint) of the sensor by adding position + let end = p5.Vector.add(position, this.v); + //{!1} How far is it from the food center + let d = end.dist(food.position); + //{!1} If it is within the radius light up the sensor + if (d < food.r) { + // The further into the center the food, the more the sensor activates + this.value = 1; + } else { + // this.value = 0; + } + } +} \ No newline at end of file diff --git a/content/examples/10_nn/neuroevolution_ecosystem/sketch.js b/content/examples/10_nn/neuroevolution_ecosystem/sketch.js new file mode 100644 index 0000000..9c3a4f6 --- /dev/null +++ b/content/examples/10_nn/neuroevolution_ecosystem/sketch.js @@ -0,0 +1,39 @@ +let bloops = []; +let timeSlider; +let food = []; + +function setup() { + createCanvas(640, 240); + ml5.tf.setBackend("cpu"); + for (let i = 0; i < 20; i++) { + bloops[i] = new Creature(random(width), random(height)); + } + for (let i = 0; i < 8; i++) { + food[i] = new Food(); + } + timeSlider = createSlider(1, 20, 1); +} + +function draw() { + background(255); + for (let i = 0; i < timeSlider.value(); i++) { + for (let i = bloops.length - 1; i >= 0; i--) { + bloops[i].think(); + bloops[i].eat(); + bloops[i].update(); + bloops[i].borders(); + if (bloops[i].health < 0) { + bloops.splice(i, 1); + } else if (random(1) < 0.001) { + let child = bloops[i].reproduce(); + bloops.push(child); + } + } + } + for (let treat of food) { + treat.show(); + } + for (let bloop of bloops) { + bloop.show(); + } +} diff --git a/content/examples/10_nn/neuroevolution_ecosystem/style.css b/content/examples/10_nn/neuroevolution_ecosystem/style.css new file mode 100644 index 0000000..e78b710 --- /dev/null +++ b/content/examples/10_nn/neuroevolution_ecosystem/style.css @@ -0,0 +1,8 @@ +html, +body { + margin: 0; + padding: 0; +} +canvas { + display: block; +}