class Matrix {
constructor(rows, cols) {
this.rows = rows;
this.cols = cols;
this.data = new Array(this.rows)
.fill()
.map(() => new Array(this.cols).fill(0));
}
copy() {
let matrix = new Matrix(this.rows, this.cols);
matrix.data = this.data;
return matrix;
}
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) {
Doodle.showMsg(
"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 (this.rows !== n.rows || this.cols !== n.cols) {
Doodle.showMsg(
"Columns and Rows of A must match Columns and Rows of B."
);
return;
}
return this.map((e, i, j) => e + n.data[i][j]);
}
static add(n) {
if (n instanceof Matrix) {
if (this.rows !== n.rows || this.cols !== n.cols) {
Doodle.showMsg(
"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) {
Doodle.showMsg("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) {
Doodle.showMsg(
"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(fn) {
// Apply a function to every element of matrix
for (let idx = 0; idx < this.rows; idx++) {
for (let jdx = 0; jdx < this.cols; jdx++) {
let val = this.data[idx][jdx];
this.data[idx][jdx] = fn(val, idx, jdx);
}
}
return this;
}
static map(matrix, fn) {
// Apply a function to every element of matrix
return new Matrix(matrix.rows, matrix.cols).map((e, i, j) =>
fn(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;
}
}
class NeuralLayer {
// input_nodes, nodes
constructor(parent, inputNeurons, neurons, type) {
this.parent = parent;
this.type = type;
this.neurons = neurons;
this.inputNeurons = inputNeurons;
this.addWeight();
this.addBias();
}
addWeight() {
this.weights = new Matrix(this.neurons, this.inputNeurons);
this.weights.randomize();
}
addBias() {
this.bias = new Matrix(this.neurons, 1);
this.bias.randomize();
}
predict(input_matrix) {
input_matrix = Matrix.multiply(this.weights, input_matrix);
input_matrix.add(this.bias);
input_matrix.map(this.parent.activationFunction.fn);
return input_matrix;
}
applyError(prediction, previousPrediction, current_errors) {
// Calculate the gradients for the layer
let gradients = Matrix.map(
prediction,
this.parent.activationFunction.errorFn
);
gradients.multiply(current_errors);
gradients.multiply(this.parent.learningRate);
// Calculate deltas
let previousPredictionTranspose = Matrix.transpose(previousPrediction);
let weight_deltas = Matrix.multiply(
gradients,
previousPredictionTranspose
);
//Apply Errors to the weights and the bias
this.weights.add(weight_deltas);
this.bias.add(gradients);
// Calculate the next layer errors
let weightsTranspose = Matrix.transpose(this.weights);
current_errors = Matrix.multiply(weightsTranspose, current_errors);
return current_errors;
}
}
class NeuralNetwork {
constructor(input_nodes, hidden_nodes, output_nodes) {
this.input_nodes = input_nodes;
this.layers = [];
this.output_nodes = output_nodes;
// Checking if hidden nodes is an integer
// If so convert it to an array with one entry
this.hidden_nodes = Number.isInteger(hidden_nodes)
? [hidden_nodes]
: hidden_nodes;
this.setupLayers();
this.setLearningRate();
this.addActivationFunctions();
this.setActivationFunction();
}
setupLayers() {
// Creating the Layers of the Neural Network
// Loop hidden layers except for the first and last, set manuelly
// input_nodes for NeuralLayer is always the previous Layer
this.layers.push(
new NeuralLayer(
this,
this.input_nodes,
this.hidden_nodes[0],
"Input Layer"
)
);
// Loop hidden layers except for the first and last, set manuelly
for (let idx = 1; idx < this.hidden_nodes.length; idx++) {
// input_nodes for NeuralNetworkLayer is always the previous Layer
this.layers.push(
new NeuralLayer(
this,
this.hidden_nodes[idx - 1],
this.hidden_nodes[idx],
"Hidden Layer"
)
);
}
// hidden_nodes.length is the last entry at that time
this.layers.push(
new NeuralLayer(
this,
this.hidden_nodes[this.hidden_nodes.length - 1],
this.output_nodes,
"Output Layer"
)
);
}
predict(input_array) {
input_array = Matrix.fromArray(input_array);
// Loop layer over the inputs
this.layers.forEach((layer) => {
input_array = layer.predict(input_array);
});
// Sending prediction to the caller!
return input_array.toArray();
}
setLearningRate(learningRate) {
this.learningRate = learningRate || 0.1;
}
addActivationFunctions() {
this.sigmoid = new ActivationFunction(
"Sigmoid",
(x) => 1 / (1 + Math.exp(-x)),
(y) => y * (1 - y)
);
this.tanh = new ActivationFunction(
"Tanh",
(x) => Math.tanh(x),
(y) => 1 - y * y
);
}
setActivationFunction(activationFunction) {
this.activationFunction = activationFunction
? this[activationFunction]
: this.sigmoid;
}
train(input_array, target_array) {
// Convert input arrays to matrix objects
let inputs = Matrix.fromArray(input_array);
let targets = Matrix.fromArray(target_array);
let predictions = [];
let prediction = inputs;
// Loop layer over the inputs
this.layers.forEach((layer) => {
prediction = layer.predict(prediction);
predictions.push(prediction);
});
// Last layer == output layer
let outputs = predictions[predictions.length - 1];
// Calculate the error
// ERROR = TARGETS - OUTPUTS
let current_errors = Matrix.subtract(targets, outputs);
for (let idx = this.layers.length - 1; idx >= 0; idx--) {
// Calculate deltas
current_errors = this.layers[idx].applyError(
predictions[idx],
idx === 0 ? inputs : predictions[idx - 1],
current_errors
);
}
}
serialize() {
let cache = [];
let result = JSON.stringify(this, (key, value) => {
if (typeof value === "object" && value !== null) {
if (cache.indexOf(value) !== -1) {
// Circular reference found, discard key
return;
}
// Store value in our collection
cache.push(value);
}
return value;
});
cache = null;
return result;
}
static deserialize(data) {
if (typeof data === "string") {
data = JSON.parse(data);
}
let _NeuralNetwork = new NeuralNetwork(
data.input_nodes,
data.hidden_nodes,
data.output_nodes
);
let _NeuralNetworkLayers = [];
data.layers.map((layer) => {
let _NeuralLayer = new NeuralLayer(
_NeuralNetwork,
layer.weights.cols,
layer.weights.rows
);
_NeuralLayer.weights = Matrix.deserialize(layer.weights);
_NeuralLayer.bias = Matrix.deserialize(layer.bias);
_NeuralNetworkLayers.push(_NeuralLayer);
});
_NeuralNetwork.layers = _NeuralNetworkLayers;
return _NeuralNetwork;
}
}
class ActivationFunction {
constructor(name, fn, errorFn) {
this.name = name;
this.fn = fn;
this.errorFn = errorFn;
}
}
class Doodle {
constructor() {
this.figures = [
"banana",
"airplane",
"car",
"cat",
"bat",
"mountain",
"train",
"line",
];
this.imageLength = 784;
this.totalData = 5000;
this.training = [];
this.trainingLength = 500;
this.testing = [];
this.testingLength = 500;
this.accuracies = [];
this.epochCounter = 0;
this.NeuralNetwork = null;
this.activationFunction = null;
this.learningRate = 0.1;
this.totalHiddenLayers = 1;
this.totalHiddenNeurons = 64;
this.setupLoadBytes();
}
preload() {
this.figures.forEach((fig) => {
let _fig = `${fig}s`;
let _figData = `${fig}sData`;
this[_fig] = {};
this[_figData] = loadBytes(`/uploads/hrwx/${fig}Compressed.bin`);
});
}
setup() {
this.setupCanvas();
this.loadData();
this.createNeuralNetwork();
this.refresh();
}
setupCanvas() {
createCanvas(840, 840);
background(80);
}
refresh() {
this.setupHTML();
this.setupTrain();
this.setupTest();
this.setupGuess();
this.setupClear();
this.setupSave();
this.setupLoad();
this.setupLearningRate();
this.setupHiddenLayers();
this.setupHiddenNeurons();
this.setupActivationFunction();
}
resizeCanvas() {
createCanvas(840, 840);
background(80);
}
loadData() {
this.figures.forEach((fig, idx) => {
let _fig = `${fig}s`;
let _figData = `${fig}sData`;
this.prepareData(this[_fig], this[_figData], idx);
});
this.figures.forEach((fig, idx) => {
let _fig = `${fig}s`;
this.training = this.training.concat(this[_fig].training);
this.testing = this.testing.concat(this[_fig].testing);
});
}
setupTrain() {
let me = this;
let trainLength = document.getElementById("training_length");
let trainButton = document.getElementById("train");
$(trainButton).click(function (e) {
me.showMsg(`Training for Epoch ${me.epochCounter}.`);
e.stopPropagation();
e.preventDefault();
me.trainingLength = trainLength.value;
me.trainEpoch();
me.epochCounter++;
me.refresh();
me.showMsg(`Epoch ${me.epochCounter}.`);
});
}
setupTest() {
let me = this;
let testLength = document.getElementById("testing_length");
let testButton = document.getElementById("test");
$(testButton).click(function (event) {
event.stopPropagation();
event.preventDefault();
me.testingLength = testLength.value;
let percent = me.test();
me.showGraph();
me.showMsg(`Accuracy: ${nf(percent, 2, 2)}%`);
});
}
setupGuess() {
let me = this;
let guessButton = document.getElementById("guess");
$(guessButton).click(function (event) {
event.stopPropagation();
event.preventDefault();
let inputs = [];
let doodleGuesses = [];
let img = get();
img.resize(28, 28);
img.loadPixels();
for (let idx = 0; idx < me.imageLength; idx++) {
let bright = img.pixels[idx * 4];
inputs[idx] = (255 - bright) / 255.0;
}
let guess = me.NeuralNetwork.predict(inputs);
let _guess = [...guess].sort().reverse();
for (let idx = 0; idx < min([_guess.length, 3]); idx++) {
if (idx > 3) {
break;
}
doodleGuesses.push(me.figures[guess.indexOf(_guess[idx])]);
}
me.showMsg(
`Is it a ${doodleGuesses[0]} 😅 or ${doodleGuesses[1]} 🤯 or ${doodleGuesses[2]} 😑.`
);
});
}
setupClear() {
let me = this;
let clearButton = document.getElementById("clear");
$(clearButton).click(function (event) {
event.stopPropagation();
event.preventDefault();
background(80);
});
}
setupSave() {
let me = this;
let saveButton = document.getElementById("save");
$(saveButton).click(function (event) {
event.stopPropagation();
event.preventDefault();
me.showMsg(`Saving Neural Network.`);
me.saveNeuralNetwork();
me.showMsg(`Neural Network saved.`);
});
}
setupLoad() {
let me = this;
let loadButton = document.getElementById("load");
$(loadButton).click(function (event) {
event.stopPropagation();
event.preventDefault();
me.loadNeuralNetwork();
});
}
setupLearningRate() {
let me = this;
let learningRate = document.getElementById("learning_rate");
$(learningRate).blur(function (event) {
event.stopPropagation();
event.preventDefault();
me.showMsg(`Settings learning rate ${learningRate.value}.`);
me.NeuralNetwork.setLearningRate(learningRate.value);
});
}
setupHiddenLayers() {
let me = this;
let hiddenLayer = document.getElementById("hidden_layer");
$(hiddenLayer).change(function (event) {
event.stopPropagation();
event.preventDefault();
me.showMsg(
`${
me.totalHiddenLayers > hiddenLayer.value
? `Decreasing`
: `Increasing`
} hidden layers to ${hiddenLayer.value}.`
);
me.totalHiddenLayers = parseInt(hiddenLayer.value);
me.createNeuralNetwork();
me.showMsg(`Hidden layers changed to ${hiddenLayer.value}.`);
});
}
setupHiddenNeurons() {
let me = this;
let hiddenNeurons = document.getElementById("hidden_neurons");
$(hiddenNeurons).change(function (event) {
event.stopPropagation();
event.preventDefault();
me.showMsg(
`${
me.totalHiddenNeurons > hiddenNeurons.value
? `Decreasing`
: `Increasing`
} hidden neurons to ${hiddenNeurons.value}.`
);
me.totalHiddenNeurons = parseInt(hiddenNeurons.value);
me.createNeuralNetwork();
me.showMsg(`Hidden neurons changed to ${hiddenNeurons.value}.`);
});
}
setupActivationFunction() {
let me = this;
let activationFunction = document.getElementById("activation_function");
$(activationFunction).change(function (event) {
event.stopPropagation();
event.preventDefault();
let labels = {
sigmoid: "Sigmoid",
tanh: "Tanh",
relu: "ReLU",
};
me.showMsg(
`Changing Activation Function to ${
labels[activationFunction.value]
}.`
);
me.NeuralNetwork.setActivationFunction(activationFunction.value);
me.showMsg(
`Activation Function changed to ${
labels[activationFunction.value]
}.`
);
});
}
createNeuralNetwork() {
let hiddenLayers = [];
let hiddenLayerCount = this.totalHiddenLayers + 1;
for (let idx = 0; idx < hiddenLayerCount; idx++) {
hiddenLayers.push(this.totalHiddenNeurons);
}
this.NeuralNetwork = new NeuralNetwork(
this.imageLength,
hiddenLayers,
this.figures.length
);
}
showMsg(_msg) {
let msg = document.getElementById("msg");
$(msg).text(_msg);
}
static showMsg(_msg) {
let msg = document.getElementById("msg");
$(msg).text(_msg);
}
setupHTML() {
AB.msg(
`
<style>
* {
box-sizing: border-box;
}
/* Create two equal columns that floats next to each other */
.column {
float: left;
width: 50%;
padding: 10px;
}
/* Clear floats after the columns */
.row:after {
content: "";
display: table;
clear: both;
}
</style>
<fieldset>
<legend>Message</legend>
<div id="msg">
</div>
</fieldset>
<br>
<fieldset>
<legend>Neural Network Test Accuracy</legend>
<div class="chart">
</div>
</fieldset>
<br>
<fieldset>
<legend>Neural Network</legend>
<div class="row">
<div class="column">
<button id="save">Save</button>
</div>
<div class="column">
<button id="load">Load Existing</button>
</div>
</div>
</fieldset>
<br>
<fieldset>
<legend>Neural Network Training and Testing</legend>
<div class="row">
<p>${this.epochCounter === 0 ? `Neural Network is not trained` : ``}</p>
</div>
<div class="row">
<div class="column">
<label for="training_length">Training Size:</label><br>
<input type="number" id="training_length" name="training_length" min="1" max="${
this.training.length
}" value="${this.trainingLength}">
<button id="train">Train</button>
</div>
<div class="column">
<label for="testing_length">Testing Size:</label><br>
<input type="number" id="testing_length" name="testing_length" min="1" max="${
this.testing.length
}" value="${this.testingLength}">
<button id="test">Test</button>
</div>
</div>
</fieldset>
<br>
<fieldset>
<legend>Neural Network Operations</legend>
<div class="row">
<div class="column">
<button id="guess">Guess</button>
</div>
<div class="column">
<button id="clear">Clear</button>
</div>
</div>
</fieldset>
<br>
<fieldset>
<legend>Neural Network Tweaks</legend>
<div class="row">
<div class="column">
<label for="learning_rate">Learning Rate:</label><br>
<input type="number" id="learning_rate" name="learning_rate" min="0.1" max="1" value="${
this.learningRate
}"><br><br>
<label for="activation_function">Activation Function:</label><br>
<select type="select" id="activation_function" name="activation_function" value=${
this.NeuralNetwork.activationFunction
}>
<option value="sigmoid">Sigmoid</option>
<option value="tanh">Tanh</option>
</select>
</div>
<div class="column">
<label for="hidden_layer">Total Hidden Layer:</label><br>
<input type="number" id="hidden_layer" name="hidden_layer" min="1" max="200" value="${
this.totalHiddenLayers
}"><br><br>
<label for="hidden_neurons">Total Neurons in Hidden Layer:</label><br>
<input type="number" id="hidden_neurons" name="hidden_neurons" min="1" max="200" value="${
this.totalHiddenNeurons
}">
</div>
</div>
</fieldset>
`
);
}
updateHTML() {
this.setupHTML();
}
draw() {
strokeWeight(8);
stroke(0);
if (mouseIsPressed) {
line(pmouseX, pmouseY, mouseX, mouseY);
}
}
trainEpoch() {
shuffle(this.training, true);
// Train for one epoch
for (let idx = 0; idx < this.trainingLength; idx++) {
console.log(`Training image: ${idx+1} of ${this.trainingLength}`);
let trainData = this.training[idx];
let inputs = Array.from(trainData).map((x) => x / 255);
let label = trainData.label;
let targets = Array(this.figures.length).fill(0);
targets[label] = 1;
this.NeuralNetwork.train(inputs, targets);
}
}
test() {
shuffle(this.testing, true);
let correct = 0;
this.accuracies = [];
// Train for one epoch
for (let idx = 0; idx < this.testingLength; idx++) {
let testData = this.testing[idx];
console.log(`Testing image: ${idx+1} of ${this.trainingLength}`);
if (!testData) continue;
let inputs = Array.from(testData).map((x) => x / 255);
let label = testData.label;
let guess = this.NeuralNetwork.predict(inputs);
let classification = guess.indexOf(max(guess));
this.accuracies.push(guess[label]);
if (classification === label) {
correct++;
}
}
return (100 * correct) / this.testingLength;
}
setupLoadBytes() {
p5.prototype.registerPreloadMethod("loadBytes");
p5.prototype.loadBytes = function (file, callback) {
let self = this;
let data = {};
let xhr = new XMLHttpRequest();
xhr.open("GET", file, true);
xhr.responseType = "arraybuffer";
xhr.onload = function (oEvent) {
var arrayBuffer = xhr.response;
if (arrayBuffer) {
data.bytes = new Uint8Array(arrayBuffer);
if (callback) {
callback(data);
}
self._decrementPreload();
}
};
xhr.send(null);
return data;
};
}
prepareData(category, data, label) {
category.training = [];
category.testing = [];
category.label = label;
for (let idx = 0; idx < this.totalData; idx++) {
let offset = idx * this.imageLength;
let threshold = floor(0.8 * this.totalData);
if (idx < threshold) {
category.training[idx] = data.bytes.subarray(
offset,
offset + this.imageLength
);
category.training[idx].label = label;
} else {
category.testing[idx - threshold] = data.bytes.subarray(
offset,
offset + this.imageLength
);
category.testing[idx - threshold].label = label;
}
}
}
showGraph() {
if (!this.accuracies) return;
let _data = {
data: {
labels: Array.from({ length: this.testingLength }, (e, i) => i),
datasets: [{
values: this.accuracies
}]
},
title: "Test Accuracy",
type: "line", // or 'bar', 'line', 'pie', 'percentage'
height: 300,
colors: ['red']
};
new frappe.Chart(".chart", _data);
}
saveNeuralNetwork() {
if (AB.myuser === "none") {
alert("User not logged in to save Neural Network.");
}
let data = {
NeuralNetwork: this.NeuralNetwork.serialize(),
learningRate: this.learningRate,
totalHiddenLayers: this.totalHiddenLayers,
totalHiddenNeurons: this.totalHiddenNeurons,
epochCounter: this.epochCounter
};
AB.saveData(JSON.stringify(data));
}
loadNeuralNetwork() {
let me = this;
if (AB.myuser === "none") {
alert("User not logged in to load Neural Network.");
}
AB.restoreData((data) => {
data = JSON.parse(data);
me.showMsg(`Loading existing Neural Network.`);
me.NeuralNetwork = NeuralNetwork.deserialize(data.NeuralNetwork);
me.learningRate = data.learningRate;
me.totalHiddenLayers = data.totalHiddenLayers;
me.totalHiddenNeurons = data.totalHiddenNeurons;
me.epochCounter = Number.isInteger(data.epochCounter) ? data.epochCounter : 0;
me.refresh();
me.showMsg(`Neural Network loaded.`);
});
}
}
$.getScript("https://unpkg.com/frappe-charts@latest");
window.doodle = new Doodle();
function preload() {
doodle.preload();
}
function setup() {
doodle.setup();
}
function draw() {
doodle.draw();
}
function windowResized() {
doodle.resizeCanvas();
}