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(); }