simple_neural_network_visualizer / CustomNeuralNetwork.js
richardhcli
make new folder, subfolder of git, since images is too annoying to deal with
b012e8c
Raw
History Blame Contribute Delete
8.89 kB
/**
* Helper functions for Neural Network Math
*/
const sigmoid = (z) => {
// z can be a number or a matrix (array of arrays)
if (Array.isArray(z)) {
return z.map(row => row.map(val => 1.0 / (1.0 + Math.exp(-val))));
}
return 1.0 / (1.0 + Math.exp(-z));
};
const sigmoidDerivative = (z) => {
const s = sigmoid(z);
if (Array.isArray(s)) {
return s.map(row => row.map(val => val * (1 - val)));
}
return s * (1 - s);
};
/**
* Basic Matrix Operations (Numpy equivalents)
*/
const Matrix = {
// Dot product of two matrices
dot: (a, b) => {
let result = new Array(a.length).fill(0).map(() => new Array(b[0].length).fill(0));
return result.map((row, i) => {
return row.map((val, j) => {
return a[i].reduce((sum, elm, k) => sum + (elm * b[k][j]), 0);
});
});
},
// Element-wise addition
add: (a, b) => a.map((row, i) => row.map((val, j) => val + b[i][j])),
// Element-wise subtraction
subtract: (a, b) => a.map((row, i) => row.map((val, j) => val - b[i][j])),
// Element-wise multiplication (Hadamard product)
multiply: (a, b) => a.map((row, i) => row.map((val, j) => val * b[i][j])),
// Scalar multiplication
scale: (a, factor) => a.map(row => row.map(val => val * factor)),
// Transpose
transpose: (a) => a[0].map((col, i) => a.map(row => row[i])),
// Initialize with random Gaussian (standard normal)
random: (rows, cols) => {
return new Array(rows).fill(0).map(() =>
new Array(cols).fill(0).map(() => {
// Box-Muller transform for Gaussian distribution
let u = 0, v = 0;
while(u === 0) u = Math.random();
while(v === 0) v = Math.random();
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
})
);
},
zeros: (rows, cols) => new Array(rows).fill(0).map(() => new Array(cols).fill(0)),
argmax: (matrix) => {
let flat = matrix.map(row => row[0]);
return flat.indexOf(Math.max(...flat));
}
};
class Network {
constructor(neuronCounts) {
this.numLayers = neuronCounts.length;
this.neuronCounts = neuronCounts;
// Biases for layers 1 to n (input layer 0 has no bias)
this.biases = neuronCounts.slice(1).map(count => Matrix.random(count, 1));
// Weights for layer pairs
this.weights = [];
for (let i = 0; i < neuronCounts.length - 1; i++) {
this.weights.push(Matrix.random(neuronCounts[i+1], neuronCounts[i]));
}
}
feedforward(inputVector) {
let a = inputVector; // inputVector should be a 2D array: [[val], [val]]
for (let i = 0; i < this.weights.length; i++) {
const wa_plus_b = Matrix.add(Matrix.dot(this.weights[i], a), this.biases[i]);
a = sigmoid(wa_plus_b);
}
return a;
}
// SGD(trainingData, epochs, miniBatchSize, learningRate, testData = null) {
// for (let j = 0; j < epochs; j++) {
// // Shuffle training data
// for (let i = trainingData.length - 1; i > 0; i--) {
// const k = Math.floor(Math.random() * (i + 1));
// [trainingData[i], trainingData[k]] = [trainingData[k], trainingData[i]];
// }
// // Create mini batches
// for (let k = 0; k < trainingData.length; k += miniBatchSize) {
// const miniBatch = trainingData.slice(k, k + miniBatchSize);
// this.updateMiniBatch(miniBatch, learningRate);
// }
// if (testData) {
// console.log(`Epoch ${j}: ${this.evaluate(testData)} / ${testData.length}`);
// } else {
// console.log(`Epoch ${j} complete`);
// }
// }
// }
async SGD_single_epoch(trainingData, miniBatchSize, learningRate, testData, visualizer_function) {
// Shuffle training data
for (let i = trainingData.length - 1; i > 0; i--) {
const k = Math.floor(Math.random() * (i + 1));
[trainingData[i], trainingData[k]] = [trainingData[k], trainingData[i]];
}
const minibatchCounterDiv = document.getElementById('minibatchCounter');
const totalMiniBatches = Math.ceil(trainingData.length / miniBatchSize);
let minibatchCounter = 0;
// Process mini batches asynchronously to allow UI updates
for (let k = 0; k < trainingData.length; k += miniBatchSize) {
const miniBatch = trainingData.slice(k, k + miniBatchSize);
this.updateMiniBatch(miniBatch, learningRate);
minibatchCounter++;
// Update UI every 10 batches or on last batch
//if (minibatchCounter % 10 === 0 || k + miniBatchSize >= trainingData.length) {
//if (minibatchCounterDiv) {
minibatchCounterDiv.textContent = `Mini-batch: ${minibatchCounter} / ${totalMiniBatches}`;
//}
// Allow UI to update
await new Promise(resolve => setTimeout(resolve, 0));
//}
}
// Update test data evaluation
const accuracy = this.evaluate(testData);
console.log(`Epoch complete: ${accuracy} / ${testData.length}`);
const evalDiv = document.getElementById('accuracyValue');
if (evalDiv) {
evalDiv.textContent = `${accuracy} / ${testData.length} = ${(accuracy / testData.length * 100).toFixed(2)}%`;
}
// Reset minibatch counter display
if (minibatchCounterDiv) {
minibatchCounterDiv.textContent = `Ready`;
}
// Visualizer function call
visualizer_function(this.getParameters());
}
updateMiniBatch(miniBatch, learningRate) {
let accB = this.biases.map(b => Matrix.zeros(b.length, b[0].length));
let accW = this.weights.map(w => Matrix.zeros(w.length, w[0].length));
for (const [x, y] of miniBatch) {
const [deltaGradB, deltaGradW] = this.backpropagation(x, y);
accB = accB.map((b, i) => Matrix.add(b, deltaGradB[i]));
accW = accW.map((w, i) => Matrix.add(w, deltaGradW[i]));
}
const eta_m = learningRate / miniBatch.length;
this.weights = this.weights.map((w, i) => Matrix.subtract(w, Matrix.scale(accW[i], eta_m)));
this.biases = this.biases.map((b, i) => Matrix.subtract(b, Matrix.scale(accB[i], eta_m)));
}
backpropagation(x, y) {
let gradB = this.biases.map(b => Matrix.zeros(b.length, b[0].length));
let gradW = this.weights.map(w => Matrix.zeros(w.length, w[0].length));
// Feedforward
let activation = x;
let activations = [x];
let logits = [];
for (let i = 0; i < this.weights.length; i++) {
let z = Matrix.add(Matrix.dot(this.weights[i], activation), this.biases[i]);
logits.push(z);
activation = sigmoid(z);
activations.push(activation);
}
// Backward pass
// Output error
let delta = Matrix.multiply(
this.costDerivative(activations[activations.length - 1], y),
sigmoidDerivative(logits[logits.length - 1])
);
gradB[gradB.length - 1] = delta;
gradW[gradW.length - 1] = Matrix.dot(delta, Matrix.transpose(activations[activations.length - 2]));
// Propagate backwards through layers
for (let l = 2; l < this.numLayers; l++) {
const z = logits[logits.length - l];
const sp = sigmoidDerivative(z);
delta = Matrix.multiply(
Matrix.dot(Matrix.transpose(this.weights[this.weights.length - l + 1]), delta),
sp
);
gradB[gradB.length - l] = delta;
gradW[gradW.length - l] = Matrix.dot(delta, Matrix.transpose(activations[activations.length - l - 1]));
}
return [gradB, gradW];
}
evaluate(testData) {
let correctCount = 0;
for (const [x, y] of testData) {
const output = this.feedforward(x);
//console.log(`Output; ${Matrix.argmax(output)}, Expected: ${y}; ${y.indexOf(1)}; ${Matrix.argmax(y)}`);
//y is column vector one-hot encoded, so we use argmax to get the index. indexOf assume is 1D array, this is 2D array.
if (Matrix.argmax(output) == Matrix.argmax(y)) {
correctCount++;
}
}
return correctCount;
}
costDerivative(outputActivations, y) {
return Matrix.subtract(outputActivations, y);
}
getParameters() {
return {
biases: this.biases,
weights: this.weights
};
}
}