Core JavaScript

Backpropagation with Tables

This JavaScript program demonstrates how to use backpropagation in a neural network to learn a set of data points. The tables display the weights of the training layers and the training data.

BackpropagationWithTables.html

<!DOCTYPE html>
<html>
  <head>
    <title>XoaX.net's Javascript</title>
    <script type="text/javascript" src="BackpropagationWithTables.js"></script>
    <style type="text/css">
    	th {
    		background-color: white;
    	}
    	td {
    		background-color: #E8E8E8;
    	}
    </style>
  </head>
  <body onload="Initialize()">
  </body>
</html>

BackpropagationWithTables.js

function Initialize() {
	const kiInputs = 3;
	const kiOutputs = 2;
	const kiSamples = 6;
	// Make a random set of inputs and outputs. Keep it simple and don't require them to be scaled. Leave them in the range [0, 1]
	let daaTrainingInputs = [];
	let daaTrainingOutputs = [];
	for(let i = 0; i < kiSamples; ++i) {
		daaTrainingInputs[i] = [];
		daaTrainingOutputs[i] = [];
		for(let j = 0; j < kiInputs; ++j) {
			daaTrainingInputs[i][j] = Math.random();
		}
		for(let j = 0; j < kiOutputs; ++j) {
			daaTrainingOutputs[i][j] =  Math.random();
		}
	}
	
	// Create a training data object
	let qTrainingData = new CTrainingData(daaTrainingInputs, daaTrainingOutputs);
	
	// Create the hidden neural network layer sizes definitions
	let iaHiddenLayers = [7, 4];
	
	// Create the neural network object
	let qNN = new CNeuralNetwork(qTrainingData, iaHiddenLayers);
	let qNNElement = qNN.CreateAndReturnNetworkVisualization();
	let qBody = document.getElementsByTagName("body")[0];
	qBody.appendChild(qNNElement);

	// Run through the layer array
	const kiEpochs = 10000;
	const kdLearningRate = .0001
	for (let i = 0; i < kiEpochs; ++i) {
		let dTotalLoss = qNN.Train(kdLearningRate);
		console.log("Total Loss = "+dTotalLoss);
	}
	qNN.FillInElements();
	let qDataTable = qTrainingData.CreateAndReturnDataTable();
	qBody.appendChild(qDataTable);
}

class CNeuralNetwork {
	constructor(qTrainingData, iaHiddenLayers) {
		this.mqTrainingData = qTrainingData;
		this.miaSizesIO = [];
		this.miaSizesIO[0] = qTrainingData.InputSize();
		for (let i = 0; i < iaHiddenLayers.length; ++i) {
			this.miaSizesIO[i+1] = iaHiddenLayers[i];
		}
		this.miaSizesIO[this.miaSizesIO.length] = qTrainingData.OutputSize();
		// Construct the layers
		this.mqaLayers = [];
		for (let i = 0; i < this.miaSizesIO.length - 1; ++i) {
			if (i < this.miaSizesIO.length - 2) {
				this.mqaLayers[i] = new CLayer(this.miaSizesIO[i], this.miaSizesIO[i+1], CActivation.GetActivationReLU());
			} else { // The last layer has the identity as its activation function
				this.mqaLayers[i] = new CLayer(this.miaSizesIO[i], this.miaSizesIO[i+1], CActivation.GetActivationIdentity());
			}
		}
	}
	Train(kdLearningRate) {
		let dTotalLoss = 0.0;
		const kiSampleCount = this.mqTrainingData.SampleCount();
		const kiInputSize = this.mqTrainingData.InputSize();
		const kiOutputSize = this.mqTrainingData.OutputSize();
		for (let s = 0; s < kiSampleCount; ++s) {
			let daInput = this.mqTrainingData.Input(s);
			let daOutput = this.mqTrainingData.Output(s);
			// Feedforward the inputs to get the outputs
			let daLayerOutput = daInput;
			for (let l = 0; l < this.mqaLayers.length; ++l) {
				daLayerOutput = this.mqaLayers[l].FeedForward(daLayerOutput);
			}
			// Calculate the output errors
			let daOutputErrors = [];
			for (let i = 0; i < kiOutputSize; ++i) {
				let dError = daLayerOutput[i] - daOutput[i];
				daOutputErrors[i] = dError;
				dTotalLoss += (dError*dError);
			}
			// Propagate the errors backward
			for (let l = this.mqaLayers.length - 1; l >= 0; --l) {
				daOutputErrors = this.mqaLayers[l].PropagateErrors(daOutputErrors);
			}
			// Use backprogatation to adjust the weights. The errors are held internally
			for (let l = this.mqaLayers.length - 1; l >= 0; --l) {
				this.mqaLayers[l].BackPropagationCorrection(.0001);
			}
		}
		return dTotalLoss/kiSampleCount;
	}
	CreateAndReturnNetworkVisualization() {
		let qContainer = document.createElement("div");
		for (let l = 0; l < this.mqaLayers.length; ++l) {
			let qTable = this.mqaLayers[l].CreateAndReturnLayerTable(l);
			qContainer.appendChild(qTable);
		}
		return qContainer;
	}
	FillInElements() {
		for (let l = 0; l < this.mqaLayers.length; ++l) {
			this.mqaLayers[l].FillInElements();
		}
	}
}

class CLayer {
	// Each layer can hold its last input, output, z-output (without activation), last errors
	// Add an activation
	constructor(kiIn, kiOut, qActivation) {
		// These values will be kept internally
		this.mdaErrorsIn = [];
		this.mdaInput = [];
		this.mdaZ = [];
		this.mdaOutput = [];
		this.mdaErrorsOut = [];
		this.mqaErrorsInElements = [];
		this.mqaInputElements = [];
		this.mqActivationElement = "";
		this.mqaaWBElements = [];
		this.mqaZElements = [];
		this.mqaOutputElements = [];
		this.mqaErrorsOutElements = [];
		this.mdaaWB = [];
		// The output size tells how many neurons to use
		for (let i = 0; i < kiOut; ++i) {
			this.mdaaWB[i] = [];
			this.mqaaWBElements[i] = [];
			// Add an extra value to the inputs (weights) for the bias term
			for (let j = 0; j <= kiIn; ++j) {
				this.mdaaWB[i][j] = Math.random() - .5;
			}
		}
		this.mqActivation = qActivation;
	}
	FeedForward(daInput) {
		// Copy the input
		for (let i = 0; i < daInput.length; ++i) {
			this.mdaInput[i] = daInput[i];
		}
		for (let j = 0; j < this.mdaaWB.length; ++j) {
			// Start with the bias in the last position
			this.mdaZ[j] = this.mdaaWB[j][daInput.length];
			for (let i = 0; i < daInput.length; ++i) {
				this.mdaZ[j] += this.mdaaWB[j][i]*this.mdaInput[i];
			}
			this.mdaOutput[j] = this.mqActivation.F(this.mdaZ[j]);
		}
		// Copy the output and return it
		let daOutput = [];
		for (let j = 0; j < this.mdaaWB.length; ++j) {
			daOutput[j] = this.mdaOutput[j];
		}
		return daOutput;
	}
	PropagateErrors(daErrorsOut) {
		// Copy the output errors multiplied by the devivative of the activation function
		for (let j = 0; j < daErrorsOut.length; ++j) {
			this.mdaErrorsOut[j] = daErrorsOut[j]*this.mqActivation.DF(this.mdaZ[j]);
		}
		// Propagate the errors backward to get the input errors
		let daErrorsIn = []
		for (let i = 0; i < this.mdaInput.length; ++i) {
			daErrorsIn[i] = 0.0;
			for (let j = 0; j < daErrorsOut.length; ++j) {
				daErrorsIn[i] += this.mdaaWB[j][i]*this.mdaErrorsOut[j];
			}
		}
		// Copy the input errors before returning them
		for (let i = 0; i < this.mdaaWB[0].length; ++i) {
			this.mdaErrorsIn[i] = daErrorsIn[i];
		}
		return daErrorsIn;
	}
	BackPropagationCorrection(dLearningRate) {
		// Appy these errors to adjust the weights
		for (let j = 0; j < this.mdaaWB.length; ++j) {
			for (let i = 0; i < this.mdaaWB[j].length - 1; ++i) {
				this.mdaaWB[j][i] -= dLearningRate*this.mdaErrorsOut[j]*this.mdaInput[i] ;
			}
			this.mdaaWB[j][this.mdaaWB[j].length - 1] -= dLearningRate*this.mdaErrorsOut[j];
		}
	}
	InputSize() {
		// Subtract 1 because the last value is the constant bias value.
		return this.mdaaWB[0].length - 1;
	}
	OutputSize() {
		return this.mdaaWB.length;
	}
	CreateAndReturnLayerTable(iLayerIndex) {
		const kiInputSize = this.InputSize();
		const kiOutputSize = this.OutputSize();
		// Create the table element
		let qTable = document.createElement("table");
		qTable.cellPadding = 5;
		qTable.cellSpacing = 0;
		qTable.border = 1;
		let qCaption = document.createElement("caption");
		qCaption.innerHTML = "Neural Network Layer "+iLayerIndex;
		qTable.appendChild(qCaption);
		// Add all of the rows to the table
		let qErrorsInRow = document.createElement("tr");
		qTable.appendChild(qErrorsInRow);
		let qInputHeadersRow = document.createElement("tr");
		qTable.appendChild(qInputHeadersRow);
		let qInputRow = document.createElement("tr");
		qTable.appendChild(qInputRow);
		let qNeuronHeadersRow = document.createElement("tr");
		qTable.appendChild(qNeuronHeadersRow);
		let qaNeuronsRows = [];
		for (let i = 0; i < kiOutputSize; ++i) {
			qaNeuronsRows[i] = document.createElement("tr");
			qTable.appendChild(qaNeuronsRows[i]);
		}
		// Add the errors in elements and the input values
		let qErrorsInHeader = document.createElement("th");
		qErrorsInRow.appendChild(qErrorsInHeader);
		qErrorsInHeader.innerHTML = "Errors In";
		qInputHeadersRow.appendChild(document.createElement("td"));
		let qInputHeader = document.createElement("th");
		qInputRow.appendChild(qInputHeader);
		qInputHeader.innerHTML = "Input";
		for (let i = 0; i < kiInputSize; ++i) {
			this.mqaErrorsInElements[i] = document.createElement("td");
			qErrorsInRow.appendChild(this.mqaErrorsInElements[i]);
			let qInputVariableHeader =  document.createElement("th");
			qInputVariableHeader.innerHTML = "X"+i;
			qInputHeadersRow.appendChild(qInputVariableHeader);
			this.mqaInputElements[i] = document.createElement("td");
			qInputRow.appendChild(this.mqaInputElements[i]);
		}
		// Add spacing elements to the errors in row
		let qSpaceElement = document.createElement("td");
		qSpaceElement.rowSpan = 3;
		qErrorsInRow.appendChild(qSpaceElement);
		qSpaceElement = document.createElement("td");
		qSpaceElement.colSpan = 2;
		qErrorsInRow.appendChild(qSpaceElement);
		qSpaceElement = document.createElement("td");
		qSpaceElement.rowSpan = 3;
		qErrorsInRow.appendChild(qSpaceElement);
		// Activation elements
		let qActivationHeader = document.createElement("th");
		qActivationHeader.innerHTML = "Activation Function";
		qActivationHeader.colSpan = 2;
		qInputHeadersRow.appendChild(qActivationHeader);
		this.mqActivationElement = document.createElement("td");
		this.mqActivationElement.colSpan = 2;
		qInputRow.appendChild(this.mqActivationElement);
		// Add the headers for the weights, bias, outputs, and errors out
		qNeuronHeadersRow.appendChild(document.createElement("td"));
		for (let i = 0; i < kiInputSize; ++i) {
			let qWeightHeader = document.createElement("th");
			qWeightHeader.innerHTML = "W"+i;
			qNeuronHeadersRow.appendChild(qWeightHeader);
		}
		let qBiasHeader = document.createElement("th");
		qBiasHeader.innerHTML = "Bias";
		qNeuronHeadersRow.appendChild(qBiasHeader);
		let qZHeader = document.createElement("th");
		qZHeader.innerHTML = "Z";
		qNeuronHeadersRow.appendChild(qZHeader);
		let qOutputHeader = document.createElement("th");
		qOutputHeader.innerHTML = "Output";
		qNeuronHeadersRow.appendChild(qOutputHeader);
		let qErrorsOutHeader = document.createElement("th");
		qErrorsOutHeader.innerHTML = "Errors Out";
		qNeuronHeadersRow.appendChild(qErrorsOutHeader);
		// Add the entries for the neuron weights and biases, z, output, and errors out.
		for (let i = 0; i < kiOutputSize; ++i) {
			let qCurrRow = qaNeuronsRows[i];
			let qNeuron = document.createElement("th");
			qNeuron.innerHTML = "Neuron "+i;
			qCurrRow.appendChild(qNeuron);
			// Use <= to include the bias term
			for (let j = 0; j <= kiInputSize; ++j) {
				this.mqaaWBElements[i][j] = document.createElement("td");
				qCurrRow.appendChild(this.mqaaWBElements[i][j]);
			}
			// Add in the z, output and error out terms
			this.mqaZElements[i] = document.createElement("td");
			this.mqaOutputElements[i] = document.createElement("td");
			this.mqaErrorsOutElements[i] = document.createElement("td");
			qCurrRow.appendChild(this.mqaZElements[i]);
			qCurrRow.appendChild(this.mqaOutputElements[i]);
			qCurrRow.appendChild(this.mqaErrorsOutElements[i]);
		}
		return qTable;
	}
	FillInElements() {
		const kiInputSize = this.InputSize();
		const kiOutputSize = this.OutputSize();
		for (let i = 0; i < kiInputSize; ++i) {
			this.mqaErrorsInElements[i].innerHTML = SetFixedDecimal(this.mdaErrorsIn[i]);
			this.mqaInputElements[i].innerHTML = SetFixedDecimal(this.mdaInput[i]);
		}
		for (let i = 0; i < kiOutputSize; ++i) {
			for (let j = 0; j <= kiInputSize; ++j) {
				this.mqaaWBElements[i][j].innerHTML = SetFixedDecimal(this.mdaaWB[i][j]);
			}
			this.mqaZElements[i].innerHTML = SetFixedDecimal(this.mdaZ[i]);
			this.mqaOutputElements[i].innerHTML = SetFixedDecimal(this.mdaOutput[i]);
			this.mqaErrorsOutElements[i].innerHTML = SetFixedDecimal(this.mdaErrorsOut[i]);
		}
		this.mqActivationElement.innerHTML = this.mqActivation.Name();
	}
}

function SetFixedDecimal(dX) {
	return Number.parseFloat(dX).toFixed(6);
}

class CActivation {
	constructor(fnF, fnDF, sName) {
		this.mfnF = fnF;
		this.mfnDF = fnDF;
		this.msName = sName;
	}
	F(x) {
		return this.mfnF(x);
	}
	DF(x) {
		return this.mfnDF(x);
	}
	Name() {
		return this.msName;
	}
	static GetActivationReLU() {
		return new CActivation(CActivation.ReLU,  CActivation.DerivativeReLU, "ReLU");
	}
	static GetActivationIdentity() {
		return new CActivation(CActivation.Identity,  CActivation.DerivativeIdentity, "Identity");
	}
	static ReLU(x) {
		return (x > 0.0) ? x : 0.0;
	}
	static DerivativeReLU(x) {
		return (x > 0.0) ? 1.0 : 0.0;
	}
	static Identity(x) {
		return x;
	}
	static DerivativeIdentity(x) {
		return 1;
	}
}


class CTrainingData {
	constructor(daaInputs, daaOutputs) {
		// Create internal copies of the data
		this.mdaaInputs = new Array();
		for (let i = 0; i < daaInputs.length; ++i) {
			this.mdaaInputs[i] = new Array();
			for (let j = 0; j < daaInputs[i].length; ++j) {
				this.mdaaInputs[i][j] = daaInputs[i][j];
			}
		}
		this.mdaaOutputs = new Array();
		for (let i = 0; i < daaOutputs.length; ++i) {
			this.mdaaOutputs[i] = new Array();
			for (let j = 0; j < daaOutputs[i].length; ++j) {
				this.mdaaOutputs[i][j] = daaOutputs[i][j];
			}
		}
	}
	CreateAndReturnDataTable() {
		const kiInputSize = this.InputSize();
		const kiOutputSize = this.OutputSize();
		const kiSampleCount = this.SampleCount();
		// Create a table element
		let qTable = document.createElement("table");
		qTable.cellPadding = 5;
		qTable.cellSpacing = 0;
		qTable.border = 1;
		let qCaption = document.createElement("caption");
		qCaption.innerHTML = "Training Data";
		qTable.appendChild(qCaption);
		// Add two rows for the headers
		let qMainRow = document.createElement("tr");
		qTable.appendChild(qMainRow);
		let qHeadersRow = document.createElement("tr");
		qTable.appendChild(qHeadersRow);
		// Add the first row of headers
		qMainRow.appendChild(document.createElement("td"));
		let qInputHeader = document.createElement("th");
		qInputHeader.colSpan = kiInputSize;
		qInputHeader.innerHTML = "Input";
		qMainRow.appendChild(qInputHeader);
		let qOutputHeader = document.createElement("th");
		qOutputHeader.colSpan = kiOutputSize;
		qOutputHeader.innerHTML = "Output";
		qMainRow.appendChild(qOutputHeader);
		// Add in the second row of headers
		qHeadersRow.appendChild(document.createElement("td"));
		for (let i = 0; i < kiInputSize; ++i) {
			let qInputVarHeader = document.createElement("th");
			qInputVarHeader.innerHTML = "X"+i;
			qHeadersRow.appendChild(qInputVarHeader);
		}
		for (let i = 0; i < kiOutputSize; ++i) {
			let qOutputVarHeader = document.createElement("th");
			qOutputVarHeader.innerHTML = "Z"+i;
			qHeadersRow.appendChild(qOutputVarHeader);
		}
		// Add rows for each input and output data element
		for (let i = 0; i < kiSampleCount; ++i) {
			let qCurrRowElement = document.createElement("tr");
			let qRowHeader = document.createElement("th");
			qRowHeader.innerHTML = "Data "+i;
			qCurrRowElement.appendChild(qRowHeader);
			qTable.appendChild(qCurrRowElement);
			for (let j = 0; j < kiInputSize; ++j) {
				let qDataElement = document.createElement("td");
				qDataElement.innerHTML = SetFixedDecimal(this.mdaaInputs[i][j]);
				qCurrRowElement.appendChild(qDataElement);
			}
			for (let j = 0; j < kiOutputSize; ++j) {
				let qDataElement = document.createElement("td");
				qDataElement.innerHTML = SetFixedDecimal(this.mdaaOutputs[i][j]);
				qCurrRowElement.appendChild(qDataElement);
			}
		}
		return qTable;
	}
	Input(i) {
		return this.mdaaInputs[i];
	}
	Output(i) {
		return this.mdaaOutputs[i];
	}
	InputSize() {
		return this.mdaaInputs[0].length;
	}
	SampleCount() {
		return this.mdaaInputs.length;
	}
	OutputSize() {
		return this.mdaaOutputs[0].length;
	}
}
 

Output

 
 

© 2007–2026 XoaX.net LLC. All rights reserved.