diff --git a/examples/NeuralNetwork-color-classifier/sketch.js b/examples/NeuralNetwork-color-classifier/sketch.js
index 70951381..a1369e2f 100644
--- a/examples/NeuralNetwork-color-classifier/sketch.js
+++ b/examples/NeuralNetwork-color-classifier/sketch.js
@@ -20,6 +20,11 @@ let label = "training";
function setup() {
createCanvas(640, 240);
+
+ // For this example to work across all browsers
+ // "webgl" or "cpu" needs to be set as the backend
+ ml5.setBackend("webgl");
+
rSlider = createSlider(0, 255, 255).position(10, 20);
gSlider = createSlider(0, 255, 0).position(10, 40);
bSlider = createSlider(0, 255, 0).position(10, 60);
diff --git a/examples/NeuralNetwork-mouse-gesture/index.html b/examples/NeuralNetwork-mouse-gesture/index.html
index 59b354e4..79c82c1f 100644
--- a/examples/NeuralNetwork-mouse-gesture/index.html
+++ b/examples/NeuralNetwork-mouse-gesture/index.html
@@ -4,7 +4,7 @@
-
ml5.js Neural Network Color Classifier
+ ml5.js Neural Network Gesture Classifier
diff --git a/examples/NeuralNetwork-mouse-gesture/sketch.js b/examples/NeuralNetwork-mouse-gesture/sketch.js
index 48d5c363..3f3ad5d2 100644
--- a/examples/NeuralNetwork-mouse-gesture/sketch.js
+++ b/examples/NeuralNetwork-mouse-gesture/sketch.js
@@ -17,6 +17,10 @@ let start, end;
function setup() {
createCanvas(640, 240);
+ // For this example to work across all browsers
+ // "webgl" or "cpu" needs to be set as the backend
+ ml5.setBackend("webgl");
+
// Step 2: set your neural network options
let options = {
task: "classification",
diff --git a/examples/NeuroEvolution-flappy-bird/bird.js b/examples/NeuroEvolution-flappy-bird/bird.js
new file mode 100644
index 00000000..2282822f
--- /dev/null
+++ b/examples/NeuroEvolution-flappy-bird/bird.js
@@ -0,0 +1,77 @@
+class Bird {
+ constructor(brain) {
+ if (brain) {
+ this.brain = brain;
+ } else {
+ // A bird's brain receives 4 inputs and classifies them into one of two labels
+ this.brain = ml5.neuralNetwork({
+ inputs: 4,
+ outputs: ["flap", "no flap"],
+ task: "classification",
+ neuroEvolution: true,
+ });
+ }
+
+ // The bird's position (x will be constant)
+ this.x = 50;
+ this.y = 120;
+
+ // Velocity and forces are scalar since the bird only moves along the y-axis
+ this.velocity = 0;
+ this.gravity = 0.5;
+ this.flapForce = -10;
+
+ // Adding a fitness
+ this.fitness = 0;
+ this.alive = true;
+ }
+
+ think(pipes) {
+ let nextPipe = null;
+ for (let pipe of pipes) {
+ if (pipe.x + pipe.w > this.x) {
+ nextPipe = pipe;
+ break;
+ }
+ }
+
+ let inputs = [
+ this.y / height,
+ this.velocity / height,
+ nextPipe.top / height,
+ (nextPipe.x - this.x) / width,
+ ];
+
+ let results = this.brain.classifySync(inputs);
+ if (results[0].label == "flap") {
+ this.flap();
+ }
+ }
+
+ // The bird flaps its wings
+ flap() {
+ this.velocity += this.flapForce;
+ }
+
+ update() {
+ // Add gravity
+ this.velocity += this.gravity;
+ this.y += this.velocity;
+ // Dampen velocity
+ this.velocity *= 0.95;
+
+ // Handle the "floor"
+ if (this.y > height || this.y < 0) {
+ this.alive = false;
+ }
+
+ this.fitness++;
+ }
+
+ show() {
+ strokeWeight(2);
+ stroke(0);
+ fill(127, 200);
+ circle(this.x, this.y, 16);
+ }
+}
diff --git a/examples/NeuroEvolution-flappy-bird/index.html b/examples/NeuroEvolution-flappy-bird/index.html
new file mode 100644
index 00000000..f5880928
--- /dev/null
+++ b/examples/NeuroEvolution-flappy-bird/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ ml5.js NeuroEvolution Flappy Bird
+
+
+
+
+
+
+
+
+
diff --git a/examples/NeuroEvolution-flappy-bird/pipe.js b/examples/NeuroEvolution-flappy-bird/pipe.js
new file mode 100644
index 00000000..9558ba9f
--- /dev/null
+++ b/examples/NeuroEvolution-flappy-bird/pipe.js
@@ -0,0 +1,34 @@
+class Pipe {
+ constructor() {
+ this.spacing = 100;
+ this.top = random(height - this.spacing);
+ this.bottom = this.top + this.spacing;
+ this.x = width;
+ this.w = 20;
+ this.speed = 2;
+ }
+
+ collides(bird) {
+ // Is the bird within the vertical range of the top or bottom pipe?
+ let verticalCollision = bird.y < this.top || bird.y > this.bottom;
+ // Is the bird within the horizontal range of the pipes?
+ let horizontalCollision = bird.x > this.x && bird.x < this.x + this.w;
+ // If it's both a vertical and horizontal hit, it's a hit!
+ return verticalCollision && horizontalCollision;
+ }
+
+ show() {
+ fill(0);
+ noStroke();
+ rect(this.x, 0, this.w, this.top);
+ rect(this.x, this.bottom, this.w, height - this.bottom);
+ }
+
+ update() {
+ this.x -= this.speed;
+ }
+
+ offscreen() {
+ return this.x < -this.w;
+ }
+}
diff --git a/examples/NeuroEvolution-flappy-bird/sketch.js b/examples/NeuroEvolution-flappy-bird/sketch.js
new file mode 100644
index 00000000..54d0ac13
--- /dev/null
+++ b/examples/NeuroEvolution-flappy-bird/sketch.js
@@ -0,0 +1,97 @@
+let birds = [];
+let pipes = [];
+
+function setup() {
+ createCanvas(640, 240);
+ // cpu is higher performance for tiny neural networks like in this example
+ ml5.setBackend("cpu");
+
+ for (let i = 0; i < 200; i++) {
+ birds[i] = new Bird();
+ }
+ pipes.push(new Pipe());
+}
+
+function draw() {
+ background(255);
+
+ for (let i = pipes.length - 1; i >= 0; i--) {
+ pipes[i].update();
+ pipes[i].show();
+ if (pipes[i].offscreen()) {
+ pipes.splice(i, 1);
+ }
+ }
+
+ for (let bird of birds) {
+ if (bird.alive) {
+ for (let pipe of pipes) {
+ if (pipe.collides(bird)) {
+ bird.alive = false;
+ }
+ }
+ bird.think(pipes);
+ bird.update();
+ bird.show();
+ }
+ }
+
+ if (frameCount % 100 == 0) {
+ pipes.push(new Pipe());
+ }
+
+ if (allBirdsDead()) {
+ normalizeFitness();
+ reproduction();
+ }
+}
+
+function allBirdsDead() {
+ for (let bird of birds) {
+ if (bird.alive) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function reproduction() {
+ let nextBirds = [];
+ for (let i = 0; i < birds.length; i++) {
+ let parentA = weightedSelection();
+ let parentB = weightedSelection();
+ let child = parentA.crossover(parentB);
+ child.mutate(0.01);
+ nextBirds[i] = new Bird(child);
+ }
+ birds = nextBirds;
+}
+
+// Normalize all fitness values
+function normalizeFitness() {
+ let sum = 0;
+ for (let bird of birds) {
+ sum += bird.fitness;
+ }
+ for (let bird of birds) {
+ bird.fitness = bird.fitness / sum;
+ }
+}
+
+function 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 - birds[index].fitness;
+ // Next element
+ index++;
+ }
+ // Undo moving to the next element since the finish has been reached
+ index--;
+ // Instead of returning the entire Bird object, just the brain is returned
+ return birds[index].brain;
+}
diff --git a/examples/NeuroEvolution-sensors/creature.js b/examples/NeuroEvolution-sensors/creature.js
new file mode 100644
index 00000000..7d19624c
--- /dev/null
+++ b/examples/NeuroEvolution-sensors/creature.js
@@ -0,0 +1,115 @@
+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",
+ 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/examples/NeuroEvolution-sensors/food.js b/examples/NeuroEvolution-sensors/food.js
new file mode 100644
index 00000000..ca0dc032
--- /dev/null
+++ b/examples/NeuroEvolution-sensors/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/examples/NeuroEvolution-sensors/index.html b/examples/NeuroEvolution-sensors/index.html
new file mode 100644
index 00000000..82fac344
--- /dev/null
+++ b/examples/NeuroEvolution-sensors/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ ml5.js NeuroEvolution Sensors
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/NeuroEvolution-sensors/sensor.js b/examples/NeuroEvolution-sensors/sensor.js
new file mode 100644
index 00000000..6a94edaa
--- /dev/null
+++ b/examples/NeuroEvolution-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 = 1;
+ } else {
+ // this.value = 0;
+ }
+ }
+}
diff --git a/examples/NeuroEvolution-sensors/sketch.js b/examples/NeuroEvolution-sensors/sketch.js
new file mode 100644
index 00000000..d917f05a
--- /dev/null
+++ b/examples/NeuroEvolution-sensors/sketch.js
@@ -0,0 +1,48 @@
+let bloops = [];
+let timeSlider;
+let restartButton;
+let food = [];
+
+function setup() {
+ createCanvas(640, 240);
+ // cpu is higher performance for tiny neural networks like in this example
+ ml5.setBackend("cpu");
+ restart();
+ restartButton = createButton("restart").mousePressed(restart);
+ timeSlider = createSlider(1, 20, 1);
+}
+
+function restart() {
+ bloops = [];
+ for (let i = 0; i < 20; i++) {
+ bloops[i] = new Creature(random(width), random(height));
+ }
+ food = [];
+ for (let i = 0; i < 8; i++) {
+ food[i] = new Food();
+ }
+}
+
+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/examples/NeuroEvolution-steering/creature.js b/examples/NeuroEvolution-steering/creature.js
new file mode 100644
index 00000000..2f09e500
--- /dev/null
+++ b/examples/NeuroEvolution-steering/creature.js
@@ -0,0 +1,79 @@
+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,
+ });
+ }
+ }
+
+ 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/examples/NeuroEvolution-steering/glow.js b/examples/NeuroEvolution-steering/glow.js
new file mode 100644
index 00000000..924cdb72
--- /dev/null
+++ b/examples/NeuroEvolution-steering/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/examples/NeuroEvolution-steering/index.html b/examples/NeuroEvolution-steering/index.html
new file mode 100644
index 00000000..789ba8c5
--- /dev/null
+++ b/examples/NeuroEvolution-steering/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ ml5.js NeuroEvolution Steering
+
+
+
+
+
+
+
+
+
diff --git a/examples/NeuroEvolution-steering/sketch.js b/examples/NeuroEvolution-steering/sketch.js
new file mode 100644
index 00000000..d77909a7
--- /dev/null
+++ b/examples/NeuroEvolution-steering/sketch.js
@@ -0,0 +1,82 @@
+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);
+}
+
+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() {
+ for (let creature of creatures) {
+ creature.fitness = pow(2, creature.fitness);
+ }
+ 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/src/NeuralNetwork/index.js b/src/NeuralNetwork/index.js
index c5556edf..00dfd420 100644
--- a/src/NeuralNetwork/index.js
+++ b/src/NeuralNetwork/index.js
@@ -21,7 +21,7 @@ const DEFAULTS = {
debug: false,
learningRate: 0.2,
hiddenUnits: 16,
- noTraining: false,
+ neuroEvolution: false,
};
class DiyNeuralNetwork {
constructor(options, cb) {
@@ -113,9 +113,8 @@ class DiyNeuralNetwork {
* @param {*} callback
*/
init(callback) {
- tf.setBackend("webgl");
// check if the a static model should be built based on the inputs and output properties
- if (this.options.noTraining === true) {
+ if (this.options.neuroEvolution === true) {
this.createLayersNoTraining();
}