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
        };
    }
}