Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Several changes for smoother results and more control over the NN. #116

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions examples/neuroevolution-steering/sketch.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ let foodBuffer = 50;
// How many sensors does each vehicle have?
let totalSensors = 8;
// How far can each vehicle see?
let sensorLength = 150;
let sensorLength = 50;
// What's the angle in between sensors
let sensorAngle = (Math.PI * 2) / totalSensors;

Expand Down Expand Up @@ -94,14 +94,19 @@ function draw() {
if (population.length < 20) {
for (let v of population) {
// Every vehicle has a chance of cloning itself according to score
// Argument to "clone" is probability
let newVehicle = v.clone(0.1 * v.score / record);
let probability = 0.1 * v.score / record;
let newVehicle = v.clone(probability);
// If there is a child
if (newVehicle != null) {
population.push(newVehicle);
}
}
}
// Make sure we never run out of vehicles, but favor reproduction
if (population.length <= 2) {
let vehicle = new Vehicle();
population.push(vehicle);
}
}

// Draw all the food
Expand Down
49 changes: 30 additions & 19 deletions examples/neuroevolution-steering/vehicle.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,9 @@

// Evolutionary "Steering Behavior" Simulation


// Mutation function to be passed into Vehicle's brain
function mutate(x) {
if (random(1) < 0.1) {
let offset = randomGaussian() * 0.5;
let newx = x + offset;
return newx;
} else {
return x;
}
}
// Neural Network parameters
// Mutation rate is set quite high because there is no crossover
const mutationRate = 0.25;

// This is a class for an individual sensor
// Each vehicle will have N sensors
Expand All @@ -37,7 +29,7 @@ class Vehicle {
this.velocity = createVector();
this.position = createVector(random(width), random(height));
this.r = 4;
this.maxforce = 0.1;
this.maxforce = 0.2;
this.maxspeed = 4;
this.minspeed = 0.25;
this.maxhealth = 3;
Expand All @@ -54,7 +46,7 @@ class Vehicle {
// If a brain is passed via constructor copy it
if (brain) {
this.brain = brain.copy();
this.brain.mutate(mutate);
this.mutate(mutationRate);
// Otherwise make a new brain
} else {
// inputs are all the sensors plus position and velocity info
Expand All @@ -64,10 +56,28 @@ class Vehicle {
this.brain = new NeuralNetwork(inputs, 32, 2);
}

// Health keeps vehicl alive
// Health keeps vehicle alive
this.health = 1;
}

mutate(rate) {
// Check if this should be mutated at all
if (Math.random() < rate) {
// This is how we adjust weights ever so slightly
function mutate(x) {
// Mutate only so much of the values
if (Math.random() < rate) {
var offset = randomGaussian() * 0.5;
// var offset = random(-0.1, 0.1);
var newx = x + offset;
return newx;
} else {
return x;
}
}
this.brain.mutate(mutate);
}
}

// Called each time step
update() {
Expand Down Expand Up @@ -147,11 +157,12 @@ class Vehicle {

// Create inputs
let inputs = [];
// This is goofy but these 4 inputs are mapped to distance from edges
inputs[0] = constrain(map(this.position.x, foodBuffer, 0, 0, 1), 0, 1);
inputs[1] = constrain(map(this.position.y, foodBuffer, 0, 0, 1), 0, 1);
inputs[2] = constrain(map(this.position.x, width - foodBuffer, width, 0, 1), 0, 1);
inputs[3] = constrain(map(this.position.y, height - foodBuffer, height, 0, 1), 0, 1);
// These inputs are the location of the vehicle
inputs[0] = this.position.x / width;
inputs[1] = this.position.y / height;
// These inputs are the distance of the vehicle to east- and west borders
inputs[2] = 1 - inputs[0];
inputs[3] = 1 - inputs[1];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this just equivalent to the AI learning negative weights/biases from inputs[0] and inputs[1], though?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because all input-nodes are linked directly to all hidden-nodes I would say no in this case. If the hidden-layer was a multi-dimensional array I would say yes. The main reason for this change was because it felt a bit too 'cheaty' for me I guess and the vehicles also showed some strange behavior because the input would always be 0 until they get within this border. To be clear though I am no expert in neuro evolution, in fact I learned most of what I know about this topic from watching The Coding Train :)

Copy link
Author

@iciclesoft iciclesoft Apr 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I would say no in both cases. I do however agree that duplicating inputs 0 and 1 directly into 2 and 3 would have the exact same effect, but what it does do is help solving the XOR-problem for the border.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But duplicating them exactly would have no net effect. Simplified, a NN with two equivalent inputs and one middle layer node would have two weights w_11, w_21 for example, and two biases b_1, b_2, right? If so, then couldn't you build a 1-input NN with the same behaviour by just summing the weights and biases together?

I also have no idea about these things either, which is why I asked if it was actually different!

Copy link
Author

@iciclesoft iciclesoft Apr 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright nice, so my reasoning was that with having 1 input, the NN must use a single-value to represent 3 outcomes, namely 0 and 1 for 'close to border' and 0.5 for 'not close to border'. By using 2 inputs, it can use one value to represent 2 outcomes and the other value to represent 2 outcomes. This however is wrong, as can be seen here: http://www.iciclesoft.com/preview/nn-test

The code for these testcases can be found at https://github.com/iciclesoft/NN-Test

The remaining question however is if we want to go back to just the x and y positions (like it was before) or if we want to go with the border stroke. My vote would be for the x and y positions, which they seem to pick up quite nicely after having it run for +/- one minute at 100x speed. Edit: Did some more testing today, sometimes the borders are picked up very fast, other times they seem to be ignoring the borders for quite some time.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@meiamsome So I've done some more tests, the previous tests were mainly about the ability of a certain neural network to find both the north and south borders on a plane. I've changed these tests to also include an average of training cycles it needs to make a distinction between the two borders and the 'middle ground'. Here we can actually see a difference between having one or two inputs and even a difference in the way the inputs are given.

Mostly the results I see show that having two inputs, where the second input is an invert of the first, need the least update-cycles to complete. This is usually slightly 'better' than having two of the same inputs (which is quite strange). Having just one input is usually about 30-40% slower. This is the case where the neural networks have 32 hidden nodes. To make it even more strange, when the nn has only 4 hidden nodes, the average updates needed are a lot closer to eachother.

It gets even stranger, before I had the 'allowed error rate', which is used to determine if a test is succesfully completed, at 1% instead of 2. I would say that this wouldn't affect the results a lot, since it's the same for each test, but in these tests having two of the same inputs usually required the least updates (instead of two inputs, where the second is the invert of the first).

All in all it seems that having multiple inputs, wether they are inverted or not, does help the neural network to learn about the borders quicker.

By the way I've updated both http://www.iciclesoft.com/preview/nn-test and https://github.com/iciclesoft/NN-Test if you're interested in the tests.

// These inputs are the current velocity vector
inputs[4] = this.velocity.x / this.maxspeed;
inputs[5] = this.velocity.y / this.maxspeed;
Expand Down