Code viewer for World: Doodle recognition neural ...
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();
}