Skip to content

Commit b445e55

Browse files
authored
Merge pull request #31 from ml5js/neuroevolution
NeuroEvolution
2 parents 6d2ce6e + d7cdb96 commit b445e55

File tree

17 files changed

+647
-4
lines changed

17 files changed

+647
-4
lines changed

examples/NeuralNetwork-color-classifier/sketch.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ let label = "training";
2020

2121
function setup() {
2222
createCanvas(640, 240);
23+
24+
// For this example to work across all browsers
25+
// "webgl" or "cpu" needs to be set as the backend
26+
ml5.setBackend("webgl");
27+
2328
rSlider = createSlider(0, 255, 255).position(10, 20);
2429
gSlider = createSlider(0, 255, 0).position(10, 40);
2530
bSlider = createSlider(0, 255, 0).position(10, 60);

examples/NeuralNetwork-mouse-gesture/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>ml5.js Neural Network Color Classifier</title>
7+
<title>ml5.js Neural Network Gesture Classifier</title>
88
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js"></script>
99
<script src="../../dist/ml5.js"></script>
1010
</head>

examples/NeuralNetwork-mouse-gesture/sketch.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ let start, end;
1717

1818
function setup() {
1919
createCanvas(640, 240);
20+
// For this example to work across all browsers
21+
// "webgl" or "cpu" needs to be set as the backend
22+
ml5.setBackend("webgl");
23+
2024
// Step 2: set your neural network options
2125
let options = {
2226
task: "classification",
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
class Bird {
2+
constructor(brain) {
3+
if (brain) {
4+
this.brain = brain;
5+
} else {
6+
// A bird's brain receives 4 inputs and classifies them into one of two labels
7+
this.brain = ml5.neuralNetwork({
8+
inputs: 4,
9+
outputs: ["flap", "no flap"],
10+
task: "classification",
11+
neuroEvolution: true,
12+
});
13+
}
14+
15+
// The bird's position (x will be constant)
16+
this.x = 50;
17+
this.y = 120;
18+
19+
// Velocity and forces are scalar since the bird only moves along the y-axis
20+
this.velocity = 0;
21+
this.gravity = 0.5;
22+
this.flapForce = -10;
23+
24+
// Adding a fitness
25+
this.fitness = 0;
26+
this.alive = true;
27+
}
28+
29+
think(pipes) {
30+
let nextPipe = null;
31+
for (let pipe of pipes) {
32+
if (pipe.x + pipe.w > this.x) {
33+
nextPipe = pipe;
34+
break;
35+
}
36+
}
37+
38+
let inputs = [
39+
this.y / height,
40+
this.velocity / height,
41+
nextPipe.top / height,
42+
(nextPipe.x - this.x) / width,
43+
];
44+
45+
let results = this.brain.classifySync(inputs);
46+
if (results[0].label == "flap") {
47+
this.flap();
48+
}
49+
}
50+
51+
// The bird flaps its wings
52+
flap() {
53+
this.velocity += this.flapForce;
54+
}
55+
56+
update() {
57+
// Add gravity
58+
this.velocity += this.gravity;
59+
this.y += this.velocity;
60+
// Dampen velocity
61+
this.velocity *= 0.95;
62+
63+
// Handle the "floor"
64+
if (this.y > height || this.y < 0) {
65+
this.alive = false;
66+
}
67+
68+
this.fitness++;
69+
}
70+
71+
show() {
72+
strokeWeight(2);
73+
stroke(0);
74+
fill(127, 200);
75+
circle(this.x, this.y, 16);
76+
}
77+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>ml5.js NeuroEvolution Flappy Bird</title>
8+
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js"></script>
9+
<script src="../../dist/ml5.js"></script>
10+
</head>
11+
<body>
12+
<script src="bird.js"></script>
13+
<script src="pipe.js"></script>
14+
<script src="sketch.js"></script>
15+
</body>
16+
</html>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
class Pipe {
2+
constructor() {
3+
this.spacing = 100;
4+
this.top = random(height - this.spacing);
5+
this.bottom = this.top + this.spacing;
6+
this.x = width;
7+
this.w = 20;
8+
this.speed = 2;
9+
}
10+
11+
collides(bird) {
12+
// Is the bird within the vertical range of the top or bottom pipe?
13+
let verticalCollision = bird.y < this.top || bird.y > this.bottom;
14+
// Is the bird within the horizontal range of the pipes?
15+
let horizontalCollision = bird.x > this.x && bird.x < this.x + this.w;
16+
// If it's both a vertical and horizontal hit, it's a hit!
17+
return verticalCollision && horizontalCollision;
18+
}
19+
20+
show() {
21+
fill(0);
22+
noStroke();
23+
rect(this.x, 0, this.w, this.top);
24+
rect(this.x, this.bottom, this.w, height - this.bottom);
25+
}
26+
27+
update() {
28+
this.x -= this.speed;
29+
}
30+
31+
offscreen() {
32+
return this.x < -this.w;
33+
}
34+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
let birds = [];
2+
let pipes = [];
3+
4+
function setup() {
5+
createCanvas(640, 240);
6+
// cpu is higher performance for tiny neural networks like in this example
7+
ml5.setBackend("cpu");
8+
9+
for (let i = 0; i < 200; i++) {
10+
birds[i] = new Bird();
11+
}
12+
pipes.push(new Pipe());
13+
}
14+
15+
function draw() {
16+
background(255);
17+
18+
for (let i = pipes.length - 1; i >= 0; i--) {
19+
pipes[i].update();
20+
pipes[i].show();
21+
if (pipes[i].offscreen()) {
22+
pipes.splice(i, 1);
23+
}
24+
}
25+
26+
for (let bird of birds) {
27+
if (bird.alive) {
28+
for (let pipe of pipes) {
29+
if (pipe.collides(bird)) {
30+
bird.alive = false;
31+
}
32+
}
33+
bird.think(pipes);
34+
bird.update();
35+
bird.show();
36+
}
37+
}
38+
39+
if (frameCount % 100 == 0) {
40+
pipes.push(new Pipe());
41+
}
42+
43+
if (allBirdsDead()) {
44+
normalizeFitness();
45+
reproduction();
46+
}
47+
}
48+
49+
function allBirdsDead() {
50+
for (let bird of birds) {
51+
if (bird.alive) {
52+
return false;
53+
}
54+
}
55+
return true;
56+
}
57+
58+
function reproduction() {
59+
let nextBirds = [];
60+
for (let i = 0; i < birds.length; i++) {
61+
let parentA = weightedSelection();
62+
let parentB = weightedSelection();
63+
let child = parentA.crossover(parentB);
64+
child.mutate(0.01);
65+
nextBirds[i] = new Bird(child);
66+
}
67+
birds = nextBirds;
68+
}
69+
70+
// Normalize all fitness values
71+
function normalizeFitness() {
72+
let sum = 0;
73+
for (let bird of birds) {
74+
sum += bird.fitness;
75+
}
76+
for (let bird of birds) {
77+
bird.fitness = bird.fitness / sum;
78+
}
79+
}
80+
81+
function weightedSelection() {
82+
// Start with the first element
83+
let index = 0;
84+
// Pick a starting point
85+
let start = random(1);
86+
// At the finish line?
87+
while (start > 0) {
88+
// Move a distance according to fitness
89+
start = start - birds[index].fitness;
90+
// Next element
91+
index++;
92+
}
93+
// Undo moving to the next element since the finish has been reached
94+
index--;
95+
// Instead of returning the entire Bird object, just the brain is returned
96+
return birds[index].brain;
97+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
class Creature {
2+
constructor(x, y, brain) {
3+
this.position = createVector(x, y);
4+
this.velocity = createVector(0, 0);
5+
this.acceleration = createVector(0, 0);
6+
this.fullSize = 12;
7+
this.r = this.fullSize;
8+
this.maxspeed = 2;
9+
this.sensors = [];
10+
this.health = 100;
11+
12+
let totalSensors = 15;
13+
for (let i = 0; i < totalSensors; i++) {
14+
let a = map(i, 0, totalSensors, 0, TWO_PI);
15+
let v = p5.Vector.fromAngle(a);
16+
v.mult(this.fullSize * 1.5);
17+
this.sensors[i] = new Sensor(v);
18+
}
19+
20+
if (brain) {
21+
this.brain = brain;
22+
} else {
23+
this.brain = ml5.neuralNetwork({
24+
inputs: this.sensors.length,
25+
outputs: 2,
26+
task: "regression",
27+
neuroEvolution: true,
28+
});
29+
}
30+
}
31+
32+
reproduce() {
33+
let brain = this.brain.copy();
34+
brain.mutate(0.1);
35+
return new Creature(this.position.x, this.position.y, brain);
36+
}
37+
38+
eat() {
39+
for (let i = 0; i < food.length; i++) {
40+
let d = p5.Vector.dist(this.position, food[i].position);
41+
if (d < this.r + food[i].r) {
42+
this.health += 0.5;
43+
food[i].r -= 0.05;
44+
if (food[i].r < 20) {
45+
food[i] = new Food();
46+
}
47+
}
48+
}
49+
}
50+
51+
think() {
52+
for (let i = 0; i < this.sensors.length; i++) {
53+
this.sensors[i].value = 0;
54+
for (let j = 0; j < food.length; j++) {
55+
this.sensors[i].sense(this.position, food[j]);
56+
}
57+
}
58+
let inputs = [];
59+
for (let i = 0; i < this.sensors.length; i++) {
60+
inputs[i] = this.sensors[i].value;
61+
}
62+
63+
// Predicting the force to apply
64+
const outputs = this.brain.predictSync(inputs);
65+
let angle = outputs[0].value * TWO_PI;
66+
let magnitude = outputs[1].value;
67+
let force = p5.Vector.fromAngle(angle).setMag(magnitude);
68+
this.applyForce(force);
69+
}
70+
71+
// Method to update location
72+
update() {
73+
// Update velocity
74+
this.velocity.add(this.acceleration);
75+
// Limit speed
76+
this.velocity.limit(this.maxspeed);
77+
this.position.add(this.velocity);
78+
// Reset acceleration to 0 each cycle
79+
this.acceleration.mult(0);
80+
this.health -= 0.25;
81+
}
82+
83+
// Wraparound
84+
borders() {
85+
if (this.position.x < -this.r) this.position.x = width + this.r;
86+
if (this.position.y < -this.r) this.position.y = height + this.r;
87+
if (this.position.x > width + this.r) this.position.x = -this.r;
88+
if (this.position.y > height + this.r) this.position.y = -this.r;
89+
}
90+
91+
applyForce(force) {
92+
// We could add mass here if we want A = F / M
93+
this.acceleration.add(force);
94+
}
95+
96+
show() {
97+
push();
98+
translate(this.position.x, this.position.y);
99+
for (let sensor of this.sensors) {
100+
stroke(0, this.health * 2);
101+
line(0, 0, sensor.v.x, sensor.v.y);
102+
if (sensor.value > 0) {
103+
fill(255, sensor.value * 255);
104+
stroke(0, 100);
105+
circle(sensor.v.x, sensor.v.y, 4);
106+
}
107+
}
108+
noStroke();
109+
fill(0, this.health * 2);
110+
this.r = map(this.health, 0, 100, 2, this.fullSize);
111+
this.r = constrain(this.r, 2, this.fullSize);
112+
circle(0, 0, this.r * 2);
113+
pop();
114+
}
115+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class Food {
2+
constructor() {
3+
this.position = createVector(random(width), random(height));
4+
this.r = 50;
5+
}
6+
7+
show() {
8+
noStroke();
9+
fill(0, 100);
10+
circle(this.position.x, this.position.y, this.r * 2);
11+
}
12+
}

0 commit comments

Comments
 (0)