Download CustomNeuralNetwork.js from richardhcli/simple_neural_network_visualizer: direct link, hf CLI and curl.
- Browser
- Download file 8.89 kB
-
https://huggingface.co/spaces/richardhcli/simple_neural_network_visualizer/resolve/main/CustomNeuralNetwork.js
- Command line
-
hf download hf://spaces/richardhcli/simple_neural_network_visualizer/CustomNeuralNetwork.js
-
curl -L -o CustomNeuralNetwork.js https://huggingface.co/spaces/richardhcli/simple_neural_network_visualizer/resolve/main/CustomNeuralNetwork.js
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 | |
| }; | |
| } | |
| } |