File size: 8,885 Bytes
b012e8c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | /**
* 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
};
}
} |