From c961aaa140bb02219563d70e2f6fd3337562d129 Mon Sep 17 00:00:00 2001 From: Daniel Shiffman Date: Thu, 19 Apr 2018 08:44:49 -0400 Subject: [PATCH 1/2] New neuroevolution steering example This is a new example with a lot of great help from @meiamsome and more. Discussion in this live stream starting here: https://youtu.be/emjv5tr-m7Q?t=4898 --- examples/neuroevolution-steering/index.html | 27 ++ examples/neuroevolution-steering/nn/matrix.js | 138 ++++++++++ examples/neuroevolution-steering/nn/nn.js | 184 +++++++++++++ examples/neuroevolution-steering/sketch.js | 123 +++++++++ examples/neuroevolution-steering/vehicle.js | 241 ++++++++++++++++++ 5 files changed, 713 insertions(+) create mode 100755 examples/neuroevolution-steering/index.html create mode 100644 examples/neuroevolution-steering/nn/matrix.js create mode 100644 examples/neuroevolution-steering/nn/nn.js create mode 100644 examples/neuroevolution-steering/sketch.js create mode 100644 examples/neuroevolution-steering/vehicle.js diff --git a/examples/neuroevolution-steering/index.html b/examples/neuroevolution-steering/index.html new file mode 100755 index 0000000..b5e5249 --- /dev/null +++ b/examples/neuroevolution-steering/index.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + +
+
+

+ speed: 1 +

+ +

+ debug: +

+
+ + + diff --git a/examples/neuroevolution-steering/nn/matrix.js b/examples/neuroevolution-steering/nn/matrix.js new file mode 100644 index 0000000..34b2d97 --- /dev/null +++ b/examples/neuroevolution-steering/nn/matrix.js @@ -0,0 +1,138 @@ +// let m = new Matrix(3,2); + + +class Matrix { + constructor(rows, cols) { + this.rows = rows; + this.cols = cols; + this.data = Array(this.rows).fill().map(() => Array(this.cols).fill(0)); + } + + copy() { + let m = new Matrix(this.rows, this.cols); + for (let i = 0; i < this.rows; i++) { + for (let j = 0; j < this.cols; j++) { + m.data[i][j] = this.data[i][j]; + } + } + return m; + } + + static fromArray(arr) { + return new Matrix(arr.length, 1).map((e, i) => arr[i]); + } + + static subtract(a, b) { + if (a.rows !== b.rows || a.cols !== b.cols) { + console.log('Columns and Rows of A must match Columns and Rows of B.'); + return; + } + + // Return a new Matrix a-b + return new Matrix(a.rows, a.cols) + .map((_, i, j) => a.data[i][j] - b.data[i][j]); + } + + toArray() { + let arr = []; + for (let i = 0; i < this.rows; i++) { + for (let j = 0; j < this.cols; j++) { + arr.push(this.data[i][j]); + } + } + return arr; + } + + randomize() { + return this.map(e => Math.random() * 2 - 1); + } + + add(n) { + if (n instanceof Matrix) { + if (this.rows !== n.rows || this.cols !== n.cols) { + console.log('Columns and Rows of A must match Columns and Rows of B.'); + return; + } + return this.map((e, i, j) => e + n.data[i][j]); + } else { + return this.map(e => e + n); + } + } + + static transpose(matrix) { + return new Matrix(matrix.cols, matrix.rows) + .map((_, i, j) => matrix.data[j][i]); + } + + static multiply(a, b) { + // Matrix product + if (a.cols !== b.rows) { + console.log('Columns of A must match rows of B.') + return; + } + + return new Matrix(a.rows, b.cols) + .map((e, i, j) => { + // Dot product of values in col + let sum = 0; + for (let k = 0; k < a.cols; k++) { + sum += a.data[i][k] * b.data[k][j]; + } + return sum; + }); + } + + multiply(n) { + if (n instanceof Matrix) { + if (this.rows !== n.rows || this.cols !== n.cols) { + console.log('Columns and Rows of A must match Columns and Rows of B.'); + return; + } + + // hadamard product + return this.map((e, i, j) => e * n.data[i][j]); + } else { + // Scalar product + return this.map(e => e * n); + } + } + + map(func) { + // Apply a function to every element of matrix + for (let i = 0; i < this.rows; i++) { + for (let j = 0; j < this.cols; j++) { + let val = this.data[i][j]; + this.data[i][j] = func(val, i, j); + } + } + return this; + } + + static map(matrix, func) { + // Apply a function to every element of matrix + return new Matrix(matrix.rows, matrix.cols) + .map((e, i, j) => func(matrix.data[i][j], i, j)); + } + + print() { + console.table(this.data); + return this; + } + + serialize() { + return JSON.stringify(this); + } + + static deserialize(data) { + if (typeof data == 'string') { + data = JSON.parse(data); + } + let matrix = new Matrix(data.rows, data.cols); + matrix.data = data.data; + return matrix; + } +} + +if (typeof module !== 'undefined') { + module.exports = Matrix; +} diff --git a/examples/neuroevolution-steering/nn/nn.js b/examples/neuroevolution-steering/nn/nn.js new file mode 100644 index 0000000..db53b8c --- /dev/null +++ b/examples/neuroevolution-steering/nn/nn.js @@ -0,0 +1,184 @@ +// Other techniques for learning + +class ActivationFunction { + constructor(func, dfunc) { + this.func = func; + this.dfunc = dfunc; + } +} + +let sigmoid = new ActivationFunction( + x => 1 / (1 + Math.exp(-x)), + y => y * (1 - y) +); + +let tanh = new ActivationFunction( + x => Math.tanh(x), + y => 1 - (y * y) +); + + +class NeuralNetwork { + // TODO: document what a, b, c are + constructor(a, b, c) { + if (a instanceof NeuralNetwork) { + this.input_nodes = a.input_nodes; + this.hidden_nodes = a.hidden_nodes; + this.output_nodes = a.output_nodes; + + this.weights_ih = a.weights_ih.copy(); + this.weights_ho = a.weights_ho.copy(); + + this.bias_h = a.bias_h.copy(); + this.bias_o = a.bias_o.copy(); + } else { + this.input_nodes = a; + this.hidden_nodes = b; + this.output_nodes = c; + + this.weights_ih = new Matrix(this.hidden_nodes, this.input_nodes); + this.weights_ho = new Matrix(this.output_nodes, this.hidden_nodes); + this.weights_ih.randomize(); + this.weights_ho.randomize(); + + this.bias_h = new Matrix(this.hidden_nodes, 1); + this.bias_o = new Matrix(this.output_nodes, 1); + this.bias_h.randomize(); + this.bias_o.randomize(); + } + + // TODO: copy these as well + this.setLearningRate(); + this.setActivationFunction(); + + + } + + predict(input_array) { + + // Generating the Hidden Outputs + let inputs = Matrix.fromArray(input_array); + let hidden = Matrix.multiply(this.weights_ih, inputs); + hidden.add(this.bias_h); + // activation function! + hidden.map(this.activation_function.func); + + // Generating the output's output! + let output = Matrix.multiply(this.weights_ho, hidden); + output.add(this.bias_o); + output.map(this.activation_function.func); + + // Sending back to the caller! + return output.toArray(); + } + + setLearningRate(learning_rate = 0.1) { + this.learning_rate = learning_rate; + } + + setActivationFunction(func = sigmoid) { + this.activation_function = func; + } + + train(input_array, target_array) { + // Generating the Hidden Outputs + let inputs = Matrix.fromArray(input_array); + let hidden = Matrix.multiply(this.weights_ih, inputs); + hidden.add(this.bias_h); + // activation function! + hidden.map(this.activation_function.func); + + // Generating the output's output! + let outputs = Matrix.multiply(this.weights_ho, hidden); + outputs.add(this.bias_o); + outputs.map(this.activation_function.func); + + // Convert array to matrix object + let targets = Matrix.fromArray(target_array); + + // Calculate the error + // ERROR = TARGETS - OUTPUTS + let output_errors = Matrix.subtract(targets, outputs); + + // let gradient = outputs * (1 - outputs); + // Calculate gradient + let gradients = Matrix.map(outputs, this.activation_function.dfunc); + gradients.multiply(output_errors); + gradients.multiply(this.learning_rate); + + + // Calculate deltas + let hidden_T = Matrix.transpose(hidden); + let weight_ho_deltas = Matrix.multiply(gradients, hidden_T); + + // Adjust the weights by deltas + this.weights_ho.add(weight_ho_deltas); + // Adjust the bias by its deltas (which is just the gradients) + this.bias_o.add(gradients); + + // Calculate the hidden layer errors + let who_t = Matrix.transpose(this.weights_ho); + let hidden_errors = Matrix.multiply(who_t, output_errors); + + // Calculate hidden gradient + let hidden_gradient = Matrix.map(hidden, this.activation_function.dfunc); + hidden_gradient.multiply(hidden_errors); + hidden_gradient.multiply(this.learning_rate); + + // Calcuate input->hidden deltas + let inputs_T = Matrix.transpose(inputs); + let weight_ih_deltas = Matrix.multiply(hidden_gradient, inputs_T); + + this.weights_ih.add(weight_ih_deltas); + // Adjust the bias by its deltas (which is just the gradients) + this.bias_h.add(hidden_gradient); + + // outputs.print(); + // targets.print(); + // error.print(); + } + + serialize() { + return JSON.stringify(this); + } + + static deserialize(data) { + if (typeof data == 'string') { + data = JSON.parse(data); + } + let nn = new NeuralNetwork(data.input_nodes, data.hidden_nodes, data.output_nodes); + nn.weights_ih = Matrix.deserialize(data.weights_ih); + nn.weights_ho = Matrix.deserialize(data.weights_ho); + nn.bias_h = Matrix.deserialize(data.bias_h); + nn.bias_o = Matrix.deserialize(data.bias_o); + nn.learning_rate = data.learning_rate; + return nn; + } + + + // Adding function for neuro-evolution + copy() { + return new NeuralNetwork(this); + } + + mutate(rate) { + // This is how we adjust weights ever so slightly + function mutate(x) { + 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.weights_ih.map(mutate); + this.weights_ho.map(mutate); + this.bias_h.map(mutate); + this.bias_o.map(mutate); + } + + + +} \ No newline at end of file diff --git a/examples/neuroevolution-steering/sketch.js b/examples/neuroevolution-steering/sketch.js new file mode 100644 index 0000000..cb303dc --- /dev/null +++ b/examples/neuroevolution-steering/sketch.js @@ -0,0 +1,123 @@ +// Daniel Shiffman +// Nature of Code: Intelligence and Learning +// https://github.com/shiffman/NOC-S17-2-Intelligence-Learning + +// Evolutionary "Steering Behavior" Simulation + +// An array of vehicles +let population = []; + +// An array of "food" +let food = []; + +// Checkbox to show additional info +let debug; + +// Slider to speed up simulation +let speedSlider; +let speedSpan; + +// How big is the food? +let foodRadius = 8; +// How much food should there? +let foodAmount = 25; +// Don't put food near the edge +let foodBuffer = 50; + + +// How many sensors does each vehicle have? +let totalSensors = 8; +// How far can each vehicle see? +let sensorLength = 150; +// What's the angle in between sensors +let sensorAngle = (Math.PI * 2) / totalSensors; + +let best = null; + +function setup() { + + // Add canvas and grab checkbox and slider + let canvas = createCanvas(640, 360); + canvas.parent('canvascontainer'); + debug = select('#debug'); + speedSlider = select('#speedSlider'); + speedSpan = select('#speed'); + + // Create initial population + for (let i = 0; i < 20; i++) { + population[i] = new Vehicle(); + } +} + +function draw() { + background(0); + + // How fast should we speed up + let cycles = speedSlider.value(); + speedSpan.html(cycles); + + // Variable to keep track of highest scoring vehicle + + // Run the simulation "cycles" amount of time + for (let n = 0; n < cycles; n++) { + // Always keep a minimum amount of food + while (food.length < foodAmount) { + food.push(createVector(random(foodBuffer, width - foodBuffer), random(foodBuffer, height - foodBuffer))); + } + + // Eat any food + for (let v of population) { + v.eat(food); + } + + // Go through all vehicles and find the best! + let record = -1; + for (let i = population.length - 1; i >= 0; i--) { + let v = population[i]; + // Eat the food (index 0) + v.think(food); + v.update(food); + + // If the vehicle has died, remove + if (v.dead()) { + population.splice(i, 1); + } else { + // Is it the vehicles that has lived the longest? + if (v.score > record) { + record = v.score; + best = v; + } + } + } + + // If there is less than 20 apply reproduction + 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); + // If there is a child + if (newVehicle != null) { + population.push(newVehicle); + } + } + } + } + + // Draw all the food + for (let i = 0; i < food.length; i++) { + fill(100, 255, 100, 200); + stroke(100, 255, 100); + ellipse(food[i].x, food[i].y, foodRadius * 2); + } + + // Highlight the best if in debug mode + if (debug.checked()) { + best.highlight(); + } + + // Draw all the vehicles + for (let v of population) { + v.display(); + } +} diff --git a/examples/neuroevolution-steering/vehicle.js b/examples/neuroevolution-steering/vehicle.js new file mode 100644 index 0000000..c866a40 --- /dev/null +++ b/examples/neuroevolution-steering/vehicle.js @@ -0,0 +1,241 @@ +// Daniel Shiffman +// Nature of Code 2018 +// https://github.com/shiffman/NOC-S18 + +// 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; + } +} + +// This is a class for an individual sensor +// Each vehicle will have N sensors +class Sensor { + constructor(angle) { + // The vector describes the sensor's direction + this.dir = p5.Vector.fromAngle(angle); + // This is the sensor's reading + this.val = 0; + } +} + +// This is the class for each Vehicle +class Vehicle { + // A vehicle can be from a "brain" (Neural Network) + constructor(brain) { + + // All the physics stuff + this.acceleration = createVector(); + this.velocity = createVector(); + this.position = createVector(random(width), random(height)); + this.r = 4; + this.maxforce = 0.1; + this.maxspeed = 4; + this.minspeed = 0.25; + this.maxhealth = 3; + + // This indicates how well it is doing + this.score = 0; + + // Create an array of sensors + this.sensors = []; + for (let angle = 0; angle < TWO_PI; angle += sensorAngle) { + this.sensors.push(new Sensor(angle)); + } + + // If a brain is passed via constructor copy it + if (brain) { + this.brain = brain.copy(); + this.brain.mutate(mutate); + // Otherwise make a new brain + } else { + // inputs are all the sensors plus position and velocity info + let inputs = this.sensors.length + 6; + // Arbitrary hidden layer + // 2 outputs for x and y desired velocity + this.brain = new NeuralNetwork(inputs, 32, 2); + } + + // Health keeps vehicl alive + this.health = 1; + } + + + // Called each time step + update() { + // Update velocity + this.velocity.add(this.acceleration); + // Limit speed to max + this.velocity.limit(this.maxspeed); + // Keep speed at a minimum + if (this.velocity.mag() < this.minspeed) { + this.velocity.setMag(this.minspeed); + } + // Update position + this.position.add(this.velocity); + // Reset acceleration to 0 each cycle + this.acceleration.mult(0); + + // Decrease health + this.health = constrain(this.health, 0, this.maxhealth); + this.health -= 0.005; + // Increase score + this.score += 1; + } + + // Return true if health is less than zero + // or if vehicle leaves the canvas + dead() { + return (this.health < 0 || + this.position.x > width + this.r || + this.position.x < -this.r || + this.position.y > height + this.r || + this.position.y < -this.r + ); + } + + // Make a copy of this vehicle according to probability + clone(prob) { + // Pick a random number + let r = random(1); + if (r < prob) { + // New vehicle with brain copy + return new Vehicle(this.brain); + } + // otherwise will return undefined + } + + // Function to calculate all sensor readings + // And predict a "desired velocity" + think(food) { + // All sensors start with maximum length + for (let j = 0; j < this.sensors.length; j++) { + this.sensors[j].val = sensorLength; + } + + for (let i = 0; i < food.length; i++) { + // Where is the food + let otherPosition = food[i]; + // How far away? + let dist = p5.Vector.dist(this.position, otherPosition); + // Skip if it's too far away + if (dist > sensorLength) { + continue; + } + + // What is vector pointint to food + let toFood = p5.Vector.sub(otherPosition, this.position); + + // Check all the sensors + for (let j = 0; j < this.sensors.length; j++) { + // If the relative angle of the food is in between the range + let delta = this.sensors[j].dir.angleBetween(toFood); + if (delta < sensorAngle / 2) { + // Sensor value is the closest food + this.sensors[j].val = min(this.sensors[j].val, dist); + } + } + } + + // 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 current velocity vector + inputs[4] = this.velocity.x / this.maxspeed; + inputs[5] = this.velocity.y / this.maxspeed; + // All the sensor readings + for (let j = 0; j < this.sensors.length; j++) { + inputs[j + 6] = map(this.sensors[j].val, 0, sensorLength, 1, 0); + } + + // Get two outputs + let outputs = this.brain.predict(inputs); + // Turn it into a desired velocity and apply steering formula + let desired = createVector(2 * outputs[0] - 1, 2 * outputs[1] - 1); + desired.mult(this.maxspeed); + // Craig Reynolds steering formula + let steer = p5.Vector.sub(desired, this.velocity); + steer.limit(this.maxforce); + // Apply the force + this.applyForce(steer); + } + + // Check against array of food + eat(list) { + for (let i = list.length - 1; i >= 0; i--) { + // Calculate distance + let d = p5.Vector.dist(list[i], this.position); + // If vehicle is within food radius, eat it! + if (d < foodRadius) { + list.splice(i, 1); + // Add health when it eats food + this.health++; + } + } + } + + // Add force to acceleration + applyForce(force) { + this.acceleration.add(force); + } + + display() { + // Color based on health + let green = color(0, 255, 255, 255); + let red = color(255, 0, 100, 100); + let col = lerpColor(red, green, this.health) + + push(); + // Translate to vehicle position + translate(this.position.x, this.position.y); + + // Draw lines for all the activated sensors + if (debug.checked()) { + for (let i = 0; i < this.sensors.length; i++) { + let val = this.sensors[i].val; + if (val > 0) { + stroke(col); + strokeWeight(map(val, 0, sensorLength, 4, 0)); + let position = this.sensors[i].dir; + line(0, 0, position.x * val, position.y * val); + } + } + // Display score next to each vehicle + noStroke(); + fill(255, 200); + text(int(this.score), 10, 0); + } + // Draw a triangle rotated in the direction of velocity + let theta = this.velocity.heading() + PI / 2; + rotate(theta); + // Draw the vehicle itself + fill(col); + strokeWeight(1); + stroke(col); + beginShape(); + vertex(0, -this.r * 2); + vertex(-this.r, this.r * 2); + vertex(this.r, this.r * 2); + endShape(CLOSE); + pop(); + } + + // Highlight with a grey bubble + highlight() { + fill(255, 255, 255, 50); + stroke(255); + ellipse(this.position.x, this.position.y, 32, 32); + } +} \ No newline at end of file From 8d1f077b360b9155495bc888c1d023f8906363c5 Mon Sep 17 00:00:00 2001 From: Daniel Shiffman Date: Thu, 19 Apr 2018 08:49:44 -0400 Subject: [PATCH 2/2] oops, reference this repo's library files --- examples/neuroevolution-steering/index.html | 4 +- examples/neuroevolution-steering/nn/matrix.js | 138 ------------- examples/neuroevolution-steering/nn/nn.js | 184 ------------------ 3 files changed, 2 insertions(+), 324 deletions(-) delete mode 100644 examples/neuroevolution-steering/nn/matrix.js delete mode 100644 examples/neuroevolution-steering/nn/nn.js diff --git a/examples/neuroevolution-steering/index.html b/examples/neuroevolution-steering/index.html index b5e5249..a079f88 100755 --- a/examples/neuroevolution-steering/index.html +++ b/examples/neuroevolution-steering/index.html @@ -4,8 +4,8 @@ - - + + diff --git a/examples/neuroevolution-steering/nn/matrix.js b/examples/neuroevolution-steering/nn/matrix.js deleted file mode 100644 index 34b2d97..0000000 --- a/examples/neuroevolution-steering/nn/matrix.js +++ /dev/null @@ -1,138 +0,0 @@ -// let m = new Matrix(3,2); - - -class Matrix { - constructor(rows, cols) { - this.rows = rows; - this.cols = cols; - this.data = Array(this.rows).fill().map(() => Array(this.cols).fill(0)); - } - - copy() { - let m = new Matrix(this.rows, this.cols); - for (let i = 0; i < this.rows; i++) { - for (let j = 0; j < this.cols; j++) { - m.data[i][j] = this.data[i][j]; - } - } - return m; - } - - static fromArray(arr) { - return new Matrix(arr.length, 1).map((e, i) => arr[i]); - } - - static subtract(a, b) { - if (a.rows !== b.rows || a.cols !== b.cols) { - console.log('Columns and Rows of A must match Columns and Rows of B.'); - return; - } - - // Return a new Matrix a-b - return new Matrix(a.rows, a.cols) - .map((_, i, j) => a.data[i][j] - b.data[i][j]); - } - - toArray() { - let arr = []; - for (let i = 0; i < this.rows; i++) { - for (let j = 0; j < this.cols; j++) { - arr.push(this.data[i][j]); - } - } - return arr; - } - - randomize() { - return this.map(e => Math.random() * 2 - 1); - } - - add(n) { - if (n instanceof Matrix) { - if (this.rows !== n.rows || this.cols !== n.cols) { - console.log('Columns and Rows of A must match Columns and Rows of B.'); - return; - } - return this.map((e, i, j) => e + n.data[i][j]); - } else { - return this.map(e => e + n); - } - } - - static transpose(matrix) { - return new Matrix(matrix.cols, matrix.rows) - .map((_, i, j) => matrix.data[j][i]); - } - - static multiply(a, b) { - // Matrix product - if (a.cols !== b.rows) { - console.log('Columns of A must match rows of B.') - return; - } - - return new Matrix(a.rows, b.cols) - .map((e, i, j) => { - // Dot product of values in col - let sum = 0; - for (let k = 0; k < a.cols; k++) { - sum += a.data[i][k] * b.data[k][j]; - } - return sum; - }); - } - - multiply(n) { - if (n instanceof Matrix) { - if (this.rows !== n.rows || this.cols !== n.cols) { - console.log('Columns and Rows of A must match Columns and Rows of B.'); - return; - } - - // hadamard product - return this.map((e, i, j) => e * n.data[i][j]); - } else { - // Scalar product - return this.map(e => e * n); - } - } - - map(func) { - // Apply a function to every element of matrix - for (let i = 0; i < this.rows; i++) { - for (let j = 0; j < this.cols; j++) { - let val = this.data[i][j]; - this.data[i][j] = func(val, i, j); - } - } - return this; - } - - static map(matrix, func) { - // Apply a function to every element of matrix - return new Matrix(matrix.rows, matrix.cols) - .map((e, i, j) => func(matrix.data[i][j], i, j)); - } - - print() { - console.table(this.data); - return this; - } - - serialize() { - return JSON.stringify(this); - } - - static deserialize(data) { - if (typeof data == 'string') { - data = JSON.parse(data); - } - let matrix = new Matrix(data.rows, data.cols); - matrix.data = data.data; - return matrix; - } -} - -if (typeof module !== 'undefined') { - module.exports = Matrix; -} diff --git a/examples/neuroevolution-steering/nn/nn.js b/examples/neuroevolution-steering/nn/nn.js deleted file mode 100644 index db53b8c..0000000 --- a/examples/neuroevolution-steering/nn/nn.js +++ /dev/null @@ -1,184 +0,0 @@ -// Other techniques for learning - -class ActivationFunction { - constructor(func, dfunc) { - this.func = func; - this.dfunc = dfunc; - } -} - -let sigmoid = new ActivationFunction( - x => 1 / (1 + Math.exp(-x)), - y => y * (1 - y) -); - -let tanh = new ActivationFunction( - x => Math.tanh(x), - y => 1 - (y * y) -); - - -class NeuralNetwork { - // TODO: document what a, b, c are - constructor(a, b, c) { - if (a instanceof NeuralNetwork) { - this.input_nodes = a.input_nodes; - this.hidden_nodes = a.hidden_nodes; - this.output_nodes = a.output_nodes; - - this.weights_ih = a.weights_ih.copy(); - this.weights_ho = a.weights_ho.copy(); - - this.bias_h = a.bias_h.copy(); - this.bias_o = a.bias_o.copy(); - } else { - this.input_nodes = a; - this.hidden_nodes = b; - this.output_nodes = c; - - this.weights_ih = new Matrix(this.hidden_nodes, this.input_nodes); - this.weights_ho = new Matrix(this.output_nodes, this.hidden_nodes); - this.weights_ih.randomize(); - this.weights_ho.randomize(); - - this.bias_h = new Matrix(this.hidden_nodes, 1); - this.bias_o = new Matrix(this.output_nodes, 1); - this.bias_h.randomize(); - this.bias_o.randomize(); - } - - // TODO: copy these as well - this.setLearningRate(); - this.setActivationFunction(); - - - } - - predict(input_array) { - - // Generating the Hidden Outputs - let inputs = Matrix.fromArray(input_array); - let hidden = Matrix.multiply(this.weights_ih, inputs); - hidden.add(this.bias_h); - // activation function! - hidden.map(this.activation_function.func); - - // Generating the output's output! - let output = Matrix.multiply(this.weights_ho, hidden); - output.add(this.bias_o); - output.map(this.activation_function.func); - - // Sending back to the caller! - return output.toArray(); - } - - setLearningRate(learning_rate = 0.1) { - this.learning_rate = learning_rate; - } - - setActivationFunction(func = sigmoid) { - this.activation_function = func; - } - - train(input_array, target_array) { - // Generating the Hidden Outputs - let inputs = Matrix.fromArray(input_array); - let hidden = Matrix.multiply(this.weights_ih, inputs); - hidden.add(this.bias_h); - // activation function! - hidden.map(this.activation_function.func); - - // Generating the output's output! - let outputs = Matrix.multiply(this.weights_ho, hidden); - outputs.add(this.bias_o); - outputs.map(this.activation_function.func); - - // Convert array to matrix object - let targets = Matrix.fromArray(target_array); - - // Calculate the error - // ERROR = TARGETS - OUTPUTS - let output_errors = Matrix.subtract(targets, outputs); - - // let gradient = outputs * (1 - outputs); - // Calculate gradient - let gradients = Matrix.map(outputs, this.activation_function.dfunc); - gradients.multiply(output_errors); - gradients.multiply(this.learning_rate); - - - // Calculate deltas - let hidden_T = Matrix.transpose(hidden); - let weight_ho_deltas = Matrix.multiply(gradients, hidden_T); - - // Adjust the weights by deltas - this.weights_ho.add(weight_ho_deltas); - // Adjust the bias by its deltas (which is just the gradients) - this.bias_o.add(gradients); - - // Calculate the hidden layer errors - let who_t = Matrix.transpose(this.weights_ho); - let hidden_errors = Matrix.multiply(who_t, output_errors); - - // Calculate hidden gradient - let hidden_gradient = Matrix.map(hidden, this.activation_function.dfunc); - hidden_gradient.multiply(hidden_errors); - hidden_gradient.multiply(this.learning_rate); - - // Calcuate input->hidden deltas - let inputs_T = Matrix.transpose(inputs); - let weight_ih_deltas = Matrix.multiply(hidden_gradient, inputs_T); - - this.weights_ih.add(weight_ih_deltas); - // Adjust the bias by its deltas (which is just the gradients) - this.bias_h.add(hidden_gradient); - - // outputs.print(); - // targets.print(); - // error.print(); - } - - serialize() { - return JSON.stringify(this); - } - - static deserialize(data) { - if (typeof data == 'string') { - data = JSON.parse(data); - } - let nn = new NeuralNetwork(data.input_nodes, data.hidden_nodes, data.output_nodes); - nn.weights_ih = Matrix.deserialize(data.weights_ih); - nn.weights_ho = Matrix.deserialize(data.weights_ho); - nn.bias_h = Matrix.deserialize(data.bias_h); - nn.bias_o = Matrix.deserialize(data.bias_o); - nn.learning_rate = data.learning_rate; - return nn; - } - - - // Adding function for neuro-evolution - copy() { - return new NeuralNetwork(this); - } - - mutate(rate) { - // This is how we adjust weights ever so slightly - function mutate(x) { - 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.weights_ih.map(mutate); - this.weights_ho.map(mutate); - this.bias_h.map(mutate); - this.bias_o.map(mutate); - } - - - -} \ No newline at end of file