richardhcli commited on
Commit
b012e8c
·
1 Parent(s): 52e76be

make new folder, subfolder of git, since images is too annoying to deal with

Browse files
Files changed (12) hide show
  1. ConnectionManager.js +230 -0
  2. CustomNeuralNetwork.js +234 -0
  3. LICENSE +21 -0
  4. MNIST_dataset.js +142 -0
  5. NetworkToDisplay.js +191 -0
  6. README.md +271 -11
  7. SceneManager.js +212 -0
  8. SphereManager.js +225 -0
  9. index.html +193 -18
  10. main.js +86 -0
  11. preprocessing.js +65 -0
  12. style.css +0 -28
ConnectionManager.js ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ConnectionManager.js
3
+ * Manages connections (lines) between spheres
4
+ */
5
+
6
+ class ConnectionManager {
7
+ constructor(scene) {
8
+ this.scene = scene;
9
+ this.connections = new Map();
10
+ this.connectionLines = new Map();
11
+ this.materials = new Map();
12
+ }
13
+
14
+ /**
15
+ * Create a connection between two spheres
16
+ * @param {string} id - Unique identifier for this connection
17
+ * @param {THREE.Vector3} startPos - Starting position
18
+ * @param {THREE.Vector3} endPos - Ending position
19
+ * @param {object} config - Configuration object
20
+ * @param {string} config.color - Hex color (default: '#00ff00')
21
+ * @param {number} config.lineWidth - Line width (default: 2)
22
+ * @param {string} config.fromSphere - Name of source sphere
23
+ * @param {string} config.toSphere - Name of target sphere
24
+ * @returns {THREE.Line} The created line object
25
+ */
26
+ addConnection(id, startPos, endPos, config = {}) {
27
+ // Prevent duplicate IDs
28
+ if (this.connections.has(id)) {
29
+ console.warn(`Connection "${id}" already exists.`);
30
+ return null;
31
+ }
32
+
33
+ const color = config.color || '#00ff00';
34
+ const lineWidth = config.lineWidth || 2;
35
+
36
+ // Create geometry
37
+ const geometry = new THREE.BufferGeometry();
38
+ geometry.setAttribute('position', new THREE.BufferAttribute(
39
+ new Float32Array([
40
+ startPos.x, startPos.y, startPos.z,
41
+ endPos.x, endPos.y, endPos.z
42
+ ]), 3
43
+ ));
44
+
45
+ // Create material
46
+ const material = new THREE.LineBasicMaterial({
47
+ color: new THREE.Color(color),
48
+ linewidth: lineWidth,
49
+ transparent: true,
50
+ opacity: 0.8
51
+ });
52
+
53
+ // Create line
54
+ const line = new THREE.Line(geometry, material);
55
+ line.name = id;
56
+ line.userData = {
57
+ connectionId: id,
58
+ fromSphere: config.fromSphere,
59
+ toSphere: config.toSphere,
60
+ color: color,
61
+ lineWidth: lineWidth,
62
+ startPos: startPos.clone(),
63
+ endPos: endPos.clone()
64
+ };
65
+
66
+ // Add to scene
67
+ this.scene.add(line);
68
+ this.connections.set(id, line.userData);
69
+ this.connectionLines.set(id, line);
70
+ this.materials.set(id, material);
71
+
72
+ // console.log(`✓ Connection added: ${id} (${config.fromSphere} -> ${config.toSphere})`);
73
+ return line;
74
+ }
75
+
76
+ /**
77
+ * Remove a connection
78
+ * @param {string} id - Connection ID
79
+ */
80
+ removeConnection(id) {
81
+ const line = this.connectionLines.get(id);
82
+ if (!line) {
83
+ console.warn(`Connection "${id}" not found.`);
84
+ return;
85
+ }
86
+
87
+ // Remove from scene
88
+ this.scene.remove(line);
89
+
90
+ // Clean up geometry and material
91
+ const geometry = line.geometry;
92
+ const material = this.materials.get(id);
93
+ if (geometry) geometry.dispose();
94
+ if (material) material.dispose();
95
+
96
+ // Remove from maps
97
+ this.connections.delete(id);
98
+ this.connectionLines.delete(id);
99
+ this.materials.delete(id);
100
+
101
+ // console.log(`✓ Connection removed: ${id}`);
102
+ }
103
+
104
+ /**
105
+ * Update connection position (useful for dynamic connections)
106
+ * @param {string} id - Connection ID
107
+ * @param {THREE.Vector3} startPos - New start position
108
+ * @param {THREE.Vector3} endPos - New end position
109
+ */
110
+ updateConnectionPositions(id, startPos, endPos) {
111
+ const line = this.connectionLines.get(id);
112
+ if (!line) return;
113
+
114
+ const positions = line.geometry.attributes.position.array;
115
+ positions[0] = startPos.x;
116
+ positions[1] = startPos.y;
117
+ positions[2] = startPos.z;
118
+ positions[3] = endPos.x;
119
+ positions[4] = endPos.y;
120
+ positions[5] = endPos.z;
121
+
122
+ line.geometry.attributes.position.needsUpdate = true;
123
+ line.userData.startPos = startPos.clone();
124
+ line.userData.endPos = endPos.clone();
125
+ }
126
+
127
+ /**
128
+ * Update connection color
129
+ * @param {string} id - Connection ID
130
+ * @param {string} newColor - New hex color
131
+ */
132
+ updateConnectionColor(id, newColor) {
133
+ const material = this.materials.get(id);
134
+ if (!material) return;
135
+
136
+ material.color.set(new THREE.Color(newColor));
137
+ this.connections.get(id).color = newColor;
138
+ }
139
+
140
+ /**
141
+ * Get connection by ID
142
+ * @param {string} id - Connection ID
143
+ * @returns {Object} Connection data
144
+ */
145
+ getConnection(id) {
146
+ return this.connections.get(id);
147
+ }
148
+
149
+ /**
150
+ * Get all connections
151
+ * @returns {Array} Array of all connections
152
+ */
153
+ getAllConnections() {
154
+ return Array.from(this.connections.entries()).map(([id, data]) => ({
155
+ id,
156
+ ...data,
157
+ line: this.connectionLines.get(id)
158
+ }));
159
+ }
160
+
161
+ /**
162
+ * Get connections for a specific sphere
163
+ * @param {string} sphereName - Name of the sphere
164
+ * @returns {Array} Connections connected to this sphere
165
+ */
166
+ getConnectionsForSphere(sphereName) {
167
+ return Array.from(this.connections.values()).filter(conn =>
168
+ conn.fromSphere === sphereName || conn.toSphere === sphereName
169
+ );
170
+ }
171
+
172
+ /**
173
+ * Remove all connections for a sphere (when sphere is deleted)
174
+ * @param {string} sphereName - Name of the sphere
175
+ */
176
+ removeConnectionsForSphere(sphereName) {
177
+ const connectionsToRemove = Array.from(this.connections.keys()).filter(id => {
178
+ const conn = this.connections.get(id);
179
+ return conn.fromSphere === sphereName || conn.toSphere === sphereName;
180
+ });
181
+
182
+ connectionsToRemove.forEach(id => this.removeConnection(id));
183
+ }
184
+
185
+ /**
186
+ * Add a pulse effect to a connection
187
+ * @param {string} id - Connection ID
188
+ * @param {number} duration - Duration of pulse in ms
189
+ */
190
+ pulseConnection(id, duration = 500) {
191
+ const material = this.materials.get(id);
192
+ if (!material) return;
193
+
194
+ const startOpacity = material.opacity;
195
+ const startTime = Date.now();
196
+
197
+ const animate = () => {
198
+ const elapsed = Date.now() - startTime;
199
+ const progress = (elapsed % duration) / duration;
200
+ const opacityPulse = Math.sin(progress * Math.PI * 2);
201
+
202
+ material.opacity = startOpacity + opacityPulse * 0.2;
203
+
204
+ if (elapsed < duration * 2) {
205
+ requestAnimationFrame(animate);
206
+ } else {
207
+ material.opacity = startOpacity;
208
+ }
209
+ };
210
+
211
+ animate();
212
+ }
213
+
214
+ /**
215
+ * Clear all connections
216
+ */
217
+ clearAll() {
218
+ const ids = Array.from(this.connections.keys());
219
+ ids.forEach(id => this.removeConnection(id));
220
+ console.log('✓ All connections cleared');
221
+ }
222
+
223
+ /**
224
+ * Get connection count
225
+ * @returns {number}
226
+ */
227
+ getCount() {
228
+ return this.connections.size;
229
+ }
230
+ }
CustomNeuralNetwork.js ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Helper functions for Neural Network Math
3
+ */
4
+ const sigmoid = (z) => {
5
+ // z can be a number or a matrix (array of arrays)
6
+ if (Array.isArray(z)) {
7
+ return z.map(row => row.map(val => 1.0 / (1.0 + Math.exp(-val))));
8
+ }
9
+ return 1.0 / (1.0 + Math.exp(-z));
10
+ };
11
+
12
+ const sigmoidDerivative = (z) => {
13
+ const s = sigmoid(z);
14
+ if (Array.isArray(s)) {
15
+ return s.map(row => row.map(val => val * (1 - val)));
16
+ }
17
+ return s * (1 - s);
18
+ };
19
+
20
+ /**
21
+ * Basic Matrix Operations (Numpy equivalents)
22
+ */
23
+ const Matrix = {
24
+ // Dot product of two matrices
25
+ dot: (a, b) => {
26
+ let result = new Array(a.length).fill(0).map(() => new Array(b[0].length).fill(0));
27
+ return result.map((row, i) => {
28
+ return row.map((val, j) => {
29
+ return a[i].reduce((sum, elm, k) => sum + (elm * b[k][j]), 0);
30
+ });
31
+ });
32
+ },
33
+ // Element-wise addition
34
+ add: (a, b) => a.map((row, i) => row.map((val, j) => val + b[i][j])),
35
+ // Element-wise subtraction
36
+ subtract: (a, b) => a.map((row, i) => row.map((val, j) => val - b[i][j])),
37
+ // Element-wise multiplication (Hadamard product)
38
+ multiply: (a, b) => a.map((row, i) => row.map((val, j) => val * b[i][j])),
39
+ // Scalar multiplication
40
+ scale: (a, factor) => a.map(row => row.map(val => val * factor)),
41
+ // Transpose
42
+ transpose: (a) => a[0].map((col, i) => a.map(row => row[i])),
43
+ // Initialize with random Gaussian (standard normal)
44
+ random: (rows, cols) => {
45
+ return new Array(rows).fill(0).map(() =>
46
+ new Array(cols).fill(0).map(() => {
47
+ // Box-Muller transform for Gaussian distribution
48
+ let u = 0, v = 0;
49
+ while(u === 0) u = Math.random();
50
+ while(v === 0) v = Math.random();
51
+ return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
52
+ })
53
+ );
54
+ },
55
+ zeros: (rows, cols) => new Array(rows).fill(0).map(() => new Array(cols).fill(0)),
56
+ argmax: (matrix) => {
57
+ let flat = matrix.map(row => row[0]);
58
+ return flat.indexOf(Math.max(...flat));
59
+ }
60
+ };
61
+
62
+
63
+
64
+ class Network {
65
+ constructor(neuronCounts) {
66
+ this.numLayers = neuronCounts.length;
67
+ this.neuronCounts = neuronCounts;
68
+
69
+ // Biases for layers 1 to n (input layer 0 has no bias)
70
+ this.biases = neuronCounts.slice(1).map(count => Matrix.random(count, 1));
71
+
72
+ // Weights for layer pairs
73
+ this.weights = [];
74
+ for (let i = 0; i < neuronCounts.length - 1; i++) {
75
+ this.weights.push(Matrix.random(neuronCounts[i+1], neuronCounts[i]));
76
+ }
77
+ }
78
+
79
+ feedforward(inputVector) {
80
+ let a = inputVector; // inputVector should be a 2D array: [[val], [val]]
81
+ for (let i = 0; i < this.weights.length; i++) {
82
+ const wa_plus_b = Matrix.add(Matrix.dot(this.weights[i], a), this.biases[i]);
83
+ a = sigmoid(wa_plus_b);
84
+ }
85
+ return a;
86
+ }
87
+
88
+ // SGD(trainingData, epochs, miniBatchSize, learningRate, testData = null) {
89
+ // for (let j = 0; j < epochs; j++) {
90
+ // // Shuffle training data
91
+ // for (let i = trainingData.length - 1; i > 0; i--) {
92
+ // const k = Math.floor(Math.random() * (i + 1));
93
+ // [trainingData[i], trainingData[k]] = [trainingData[k], trainingData[i]];
94
+ // }
95
+
96
+ // // Create mini batches
97
+ // for (let k = 0; k < trainingData.length; k += miniBatchSize) {
98
+ // const miniBatch = trainingData.slice(k, k + miniBatchSize);
99
+ // this.updateMiniBatch(miniBatch, learningRate);
100
+ // }
101
+
102
+ // if (testData) {
103
+ // console.log(`Epoch ${j}: ${this.evaluate(testData)} / ${testData.length}`);
104
+ // } else {
105
+ // console.log(`Epoch ${j} complete`);
106
+ // }
107
+ // }
108
+ // }
109
+ async SGD_single_epoch(trainingData, miniBatchSize, learningRate, testData, visualizer_function) {
110
+ // Shuffle training data
111
+ for (let i = trainingData.length - 1; i > 0; i--) {
112
+ const k = Math.floor(Math.random() * (i + 1));
113
+ [trainingData[i], trainingData[k]] = [trainingData[k], trainingData[i]];
114
+ }
115
+
116
+ const minibatchCounterDiv = document.getElementById('minibatchCounter');
117
+ const totalMiniBatches = Math.ceil(trainingData.length / miniBatchSize);
118
+ let minibatchCounter = 0;
119
+
120
+ // Process mini batches asynchronously to allow UI updates
121
+ for (let k = 0; k < trainingData.length; k += miniBatchSize) {
122
+ const miniBatch = trainingData.slice(k, k + miniBatchSize);
123
+ this.updateMiniBatch(miniBatch, learningRate);
124
+
125
+ minibatchCounter++;
126
+
127
+ // Update UI every 10 batches or on last batch
128
+ //if (minibatchCounter % 10 === 0 || k + miniBatchSize >= trainingData.length) {
129
+ //if (minibatchCounterDiv) {
130
+ minibatchCounterDiv.textContent = `Mini-batch: ${minibatchCounter} / ${totalMiniBatches}`;
131
+ //}
132
+ // Allow UI to update
133
+ await new Promise(resolve => setTimeout(resolve, 0));
134
+ //}
135
+ }
136
+
137
+ // Update test data evaluation
138
+ const accuracy = this.evaluate(testData);
139
+ console.log(`Epoch complete: ${accuracy} / ${testData.length}`);
140
+
141
+ const evalDiv = document.getElementById('accuracyValue');
142
+ if (evalDiv) {
143
+ evalDiv.textContent = `${accuracy} / ${testData.length} = ${(accuracy / testData.length * 100).toFixed(2)}%`;
144
+ }
145
+
146
+ // Reset minibatch counter display
147
+ if (minibatchCounterDiv) {
148
+ minibatchCounterDiv.textContent = `Ready`;
149
+ }
150
+
151
+ // Visualizer function call
152
+ visualizer_function(this.getParameters());
153
+ }
154
+
155
+ updateMiniBatch(miniBatch, learningRate) {
156
+ let accB = this.biases.map(b => Matrix.zeros(b.length, b[0].length));
157
+ let accW = this.weights.map(w => Matrix.zeros(w.length, w[0].length));
158
+
159
+ for (const [x, y] of miniBatch) {
160
+ const [deltaGradB, deltaGradW] = this.backpropagation(x, y);
161
+ accB = accB.map((b, i) => Matrix.add(b, deltaGradB[i]));
162
+ accW = accW.map((w, i) => Matrix.add(w, deltaGradW[i]));
163
+ }
164
+
165
+ const eta_m = learningRate / miniBatch.length;
166
+ this.weights = this.weights.map((w, i) => Matrix.subtract(w, Matrix.scale(accW[i], eta_m)));
167
+ this.biases = this.biases.map((b, i) => Matrix.subtract(b, Matrix.scale(accB[i], eta_m)));
168
+ }
169
+
170
+ backpropagation(x, y) {
171
+ let gradB = this.biases.map(b => Matrix.zeros(b.length, b[0].length));
172
+ let gradW = this.weights.map(w => Matrix.zeros(w.length, w[0].length));
173
+
174
+ // Feedforward
175
+ let activation = x;
176
+ let activations = [x];
177
+ let logits = [];
178
+
179
+ for (let i = 0; i < this.weights.length; i++) {
180
+ let z = Matrix.add(Matrix.dot(this.weights[i], activation), this.biases[i]);
181
+ logits.push(z);
182
+ activation = sigmoid(z);
183
+ activations.push(activation);
184
+ }
185
+
186
+ // Backward pass
187
+ // Output error
188
+ let delta = Matrix.multiply(
189
+ this.costDerivative(activations[activations.length - 1], y),
190
+ sigmoidDerivative(logits[logits.length - 1])
191
+ );
192
+ gradB[gradB.length - 1] = delta;
193
+ gradW[gradW.length - 1] = Matrix.dot(delta, Matrix.transpose(activations[activations.length - 2]));
194
+
195
+ // Propagate backwards through layers
196
+ for (let l = 2; l < this.numLayers; l++) {
197
+ const z = logits[logits.length - l];
198
+ const sp = sigmoidDerivative(z);
199
+ delta = Matrix.multiply(
200
+ Matrix.dot(Matrix.transpose(this.weights[this.weights.length - l + 1]), delta),
201
+ sp
202
+ );
203
+ gradB[gradB.length - l] = delta;
204
+ gradW[gradW.length - l] = Matrix.dot(delta, Matrix.transpose(activations[activations.length - l - 1]));
205
+ }
206
+
207
+ return [gradB, gradW];
208
+ }
209
+
210
+ evaluate(testData) {
211
+ let correctCount = 0;
212
+ for (const [x, y] of testData) {
213
+ const output = this.feedforward(x);
214
+ //console.log(`Output; ${Matrix.argmax(output)}, Expected: ${y}; ${y.indexOf(1)}; ${Matrix.argmax(y)}`);
215
+ //y is column vector one-hot encoded, so we use argmax to get the index. indexOf assume is 1D array, this is 2D array.
216
+ if (Matrix.argmax(output) == Matrix.argmax(y)) {
217
+ correctCount++;
218
+ }
219
+ }
220
+ return correctCount;
221
+ }
222
+
223
+ costDerivative(outputActivations, y) {
224
+ return Matrix.subtract(outputActivations, y);
225
+ }
226
+
227
+
228
+ getParameters() {
229
+ return {
230
+ biases: this.biases,
231
+ weights: this.weights
232
+ };
233
+ }
234
+ }
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Hongcheng Li
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
MNIST_dataset.js ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * Copyright 2018 Google LLC. All Rights Reserved.
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ * =============================================================================
16
+ */
17
+
18
+ const IMAGE_SIZE = 784;
19
+ const NUM_CLASSES = 10;
20
+ const NUM_DATASET_ELEMENTS = 65000;
21
+
22
+ const NUM_TRAIN_ELEMENTS = 55000;
23
+ const NUM_TEST_ELEMENTS = NUM_DATASET_ELEMENTS - NUM_TRAIN_ELEMENTS;
24
+
25
+ const MNIST_IMAGES_SPRITE_PATH =
26
+ 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_images.png';
27
+ const MNIST_LABELS_PATH =
28
+ 'https://storage.googleapis.com/learnjs-data/model-builder/mnist_labels_uint8';
29
+
30
+ /**
31
+ * A class that fetches the sprited MNIST dataset and returns shuffled batches.
32
+ *
33
+ * NOTE: This will get much easier. For now, we do data fetching and
34
+ * manipulation manually.
35
+ */
36
+ class MnistData {
37
+ constructor() {
38
+ this.shuffledTrainIndex = 0;
39
+ this.shuffledTestIndex = 0;
40
+ }
41
+
42
+ async load() {
43
+ // Make a request for the MNIST sprited image.
44
+ const img = new Image();
45
+ const canvas = document.createElement('canvas');
46
+ const ctx = canvas.getContext('2d');
47
+ const imgRequest = new Promise((resolve, reject) => {
48
+ img.crossOrigin = '';
49
+ img.onload = () => {
50
+ img.width = img.naturalWidth;
51
+ img.height = img.naturalHeight;
52
+
53
+ const datasetBytesBuffer =
54
+ new ArrayBuffer(NUM_DATASET_ELEMENTS * IMAGE_SIZE * 4);
55
+
56
+ const chunkSize = 5000;
57
+ canvas.width = img.width;
58
+ canvas.height = chunkSize;
59
+
60
+ for (let i = 0; i < NUM_DATASET_ELEMENTS / chunkSize; i++) {
61
+ const datasetBytesView = new Float32Array(
62
+ datasetBytesBuffer, i * IMAGE_SIZE * chunkSize * 4,
63
+ IMAGE_SIZE * chunkSize);
64
+ ctx.drawImage(
65
+ img, 0, i * chunkSize, img.width, chunkSize, 0, 0, img.width,
66
+ chunkSize);
67
+
68
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
69
+
70
+ for (let j = 0; j < imageData.data.length / 4; j++) {
71
+ // All channels hold an equal value since the image is grayscale, so
72
+ // just read the red channel.
73
+ datasetBytesView[j] = imageData.data[j * 4] / 255;
74
+ }
75
+ }
76
+ this.datasetImages = new Float32Array(datasetBytesBuffer);
77
+
78
+ resolve();
79
+ };
80
+ img.src = MNIST_IMAGES_SPRITE_PATH;
81
+ });
82
+
83
+ const labelsRequest = fetch(MNIST_LABELS_PATH);
84
+ const [imgResponse, labelsResponse] =
85
+ await Promise.all([imgRequest, labelsRequest]);
86
+
87
+ this.datasetLabels = new Uint8Array(await labelsResponse.arrayBuffer());
88
+
89
+ // Create shuffled indices into the train/test set for when we select a
90
+ // random dataset element for training / validation.
91
+ this.trainIndices = tf.util.createShuffledIndices(NUM_TRAIN_ELEMENTS);
92
+ this.testIndices = tf.util.createShuffledIndices(NUM_TEST_ELEMENTS);
93
+
94
+ // Slice the the images and labels into train and test sets.
95
+ this.trainImages =
96
+ this.datasetImages.slice(0, IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
97
+ this.testImages = this.datasetImages.slice(IMAGE_SIZE * NUM_TRAIN_ELEMENTS);
98
+ this.trainLabels =
99
+ this.datasetLabels.slice(0, NUM_CLASSES * NUM_TRAIN_ELEMENTS);
100
+ this.testLabels =
101
+ this.datasetLabels.slice(NUM_CLASSES * NUM_TRAIN_ELEMENTS);
102
+ }
103
+
104
+ nextTrainBatch(batchSize) {
105
+ return this.nextBatch(
106
+ batchSize, [this.trainImages, this.trainLabels], () => {
107
+ this.shuffledTrainIndex =
108
+ (this.shuffledTrainIndex + 1) % this.trainIndices.length;
109
+ return this.trainIndices[this.shuffledTrainIndex];
110
+ });
111
+ }
112
+
113
+ nextTestBatch(batchSize) {
114
+ return this.nextBatch(batchSize, [this.testImages, this.testLabels], () => {
115
+ this.shuffledTestIndex =
116
+ (this.shuffledTestIndex + 1) % this.testIndices.length;
117
+ return this.testIndices[this.shuffledTestIndex];
118
+ });
119
+ }
120
+
121
+ nextBatch(batchSize, data, index) {
122
+ const batchImagesArray = new Float32Array(batchSize * IMAGE_SIZE);
123
+ const batchLabelsArray = new Uint8Array(batchSize * NUM_CLASSES);
124
+
125
+ for (let i = 0; i < batchSize; i++) {
126
+ const idx = index();
127
+
128
+ const image =
129
+ data[0].slice(idx * IMAGE_SIZE, idx * IMAGE_SIZE + IMAGE_SIZE);
130
+ batchImagesArray.set(image, i * IMAGE_SIZE);
131
+
132
+ const label =
133
+ data[1].slice(idx * NUM_CLASSES, idx * NUM_CLASSES + NUM_CLASSES);
134
+ batchLabelsArray.set(label, i * NUM_CLASSES);
135
+ }
136
+
137
+ const xs = tf.tensor2d(batchImagesArray, [batchSize, IMAGE_SIZE]);
138
+ const labels = tf.tensor2d(batchLabelsArray, [batchSize, NUM_CLASSES]);
139
+
140
+ return {xs, labels};
141
+ }
142
+ }
NetworkToDisplay.js ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * NetworkToDisplay - Interface between neural network and 3D visualization
3
+ * Creates spheres and manages connections directly in the SceneManager.
4
+ */
5
+ class NetworkToDisplay {
6
+ constructor(network, sceneManager, options = {}) {
7
+ this.network = network;
8
+ this.sceneManager = sceneManager;
9
+
10
+ const {
11
+ layerSpacing = 10,
12
+ neuronSpacing = 2,
13
+ neuronRadius = 0.5,
14
+ inputColor = '#ff6b6b',
15
+ hiddenColor = '#ffd93d',
16
+ outputColor = '#4ecdc4',
17
+ weightThreshold = 0.1,
18
+ lineWidth = 0.1,
19
+ opacity = 0.1
20
+ } = options;
21
+
22
+ this.layerSpacing = layerSpacing;
23
+ this.neuronSpacing = neuronSpacing;
24
+ this.neuronRadius = neuronRadius;
25
+ this.inputColor = inputColor;
26
+ this.hiddenColor = hiddenColor;
27
+ this.outputColor = outputColor;
28
+ this.weightThreshold = weightThreshold;
29
+ this.lineWidth = lineWidth;
30
+ this.opacity = opacity;
31
+ }
32
+
33
+ /**
34
+ * Create all neuron spheres and initial connections in the scene.
35
+ */
36
+ initialGeneration() {
37
+ const layers = this.network.neuronCounts;
38
+ const numLayers = layers.length;
39
+
40
+ for (let layerIdx = 0; layerIdx < numLayers; layerIdx++) {
41
+ const neuronCount = layers[layerIdx];
42
+ const x = layerIdx * this.layerSpacing - (numLayers - 1) * this.layerSpacing / 2;
43
+
44
+ let color = this.hiddenColor;
45
+ if (layerIdx === 0) color = this.inputColor;
46
+ else if (layerIdx === numLayers - 1) color = this.outputColor;
47
+
48
+ if (layerIdx === 0) {
49
+ const gridSize = Math.sqrt(neuronCount);
50
+ console.assert(
51
+ Math.floor(gridSize) * Math.floor(gridSize) === neuronCount,
52
+ `Input layer neuron count ${neuronCount} is not a perfect square`
53
+ );
54
+
55
+ const intGridSize = Math.floor(gridSize);
56
+ const halfSize = (intGridSize - 1) * this.neuronSpacing / 2;
57
+ let sphereCount = 0;
58
+
59
+ for (let row = 0; row < intGridSize; row++) {
60
+ for (let col = 0; col < intGridSize; col++) {
61
+ const y = row * this.neuronSpacing - halfSize;
62
+ const z = col * this.neuronSpacing - halfSize;
63
+
64
+ this.sceneManager.sphereManager.addSphere(`L${layerIdx}_N${sphereCount}`, {
65
+ radius: this.neuronRadius,
66
+ color: color,
67
+ position: new THREE.Vector3(x, y, z),
68
+ opacity: 1
69
+ });
70
+ sphereCount++;
71
+ }
72
+ }
73
+
74
+ console.assert(
75
+ sphereCount === neuronCount,
76
+ `Generated ${sphereCount} spheres but expected ${neuronCount} neurons in input layer`
77
+ );
78
+ } else {
79
+ const verticalOffset = -(neuronCount - 1) * this.neuronSpacing / 2;
80
+
81
+ for (let neuronIdx = 0; neuronIdx < neuronCount; neuronIdx++) {
82
+ const y = verticalOffset + neuronIdx * this.neuronSpacing;
83
+
84
+ this.sceneManager.sphereManager.addSphere(`L${layerIdx}_N${neuronIdx}`, {
85
+ radius: this.neuronRadius,
86
+ color: color,
87
+ position: new THREE.Vector3(x, y, 0),
88
+ opacity: 1
89
+ });
90
+ }
91
+ }
92
+ }
93
+
94
+ this.updateConnections();
95
+ console.log('✓ Initial generation complete');
96
+ }
97
+
98
+ /**
99
+ * Update/create/delete connections in the scene based on current weights.
100
+ */
101
+ updateConnections() {
102
+ const weightThreshold = this.weightThreshold;
103
+
104
+ for (let layerIdx = 0; layerIdx < this.network.weights.length; layerIdx++) {
105
+ const weightMatrix = this.network.weights[layerIdx];
106
+ const fromLayer = layerIdx;
107
+ const toLayer = layerIdx + 1;
108
+ const toNeuronCount = weightMatrix.length;
109
+ const fromNeuronCount = weightMatrix[0].length;
110
+
111
+ for (let toIdx = 0; toIdx < toNeuronCount; toIdx++) {
112
+ for (let fromIdx = 0; fromIdx < fromNeuronCount; fromIdx++) {
113
+ const weight = weightMatrix[toIdx][fromIdx];
114
+ const weightMagnitude = Math.abs(weight);
115
+ const connectionId = `W_L${layerIdx}_F${fromIdx}_T${toIdx}`;
116
+ const fromName = `L${fromLayer}_N${fromIdx}`;
117
+ const toName = `L${toLayer}_N${toIdx}`;
118
+
119
+ const existing = this.sceneManager.connectionManager.getConnection(connectionId);
120
+
121
+ if (weightMagnitude < weightThreshold) {
122
+ if (existing) {
123
+ this.sceneManager.connectionManager.removeConnection(connectionId);
124
+ }
125
+ continue;
126
+ }
127
+
128
+ const connectionColorThis = NetworkToDisplay.weightToColor(weight);
129
+
130
+ if (existing) {
131
+ this.sceneManager.connectionManager.updateConnectionColor(connectionId, connectionColorThis);
132
+ } else {
133
+ const fromSphere = this.sceneManager.sphereManager.getSphere(fromName);
134
+ const toSphere = this.sceneManager.sphereManager.getSphere(toName);
135
+
136
+ if (fromSphere && toSphere) {
137
+ this.sceneManager.connectionManager.addConnection(
138
+ connectionId,
139
+ fromSphere.position,
140
+ toSphere.position,
141
+ {
142
+ color: connectionColorThis,
143
+ lineWidth: this.lineWidth,
144
+ opacity: this.opacity,
145
+ fromSphere: fromName,
146
+ toSphere: toName,
147
+ weight: weight
148
+ }
149
+ );
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Highlight a specific neuron activation
159
+ * @param {number} layerIdx - Layer index
160
+ * @param {number} neuronIdx - Neuron index
161
+ * @param {number} activation - Activation value (0-1)
162
+ */
163
+ highlightNeuron(layerIdx, neuronIdx, activation) {
164
+ const sphereName = `L${layerIdx}_N${neuronIdx}`;
165
+ const sphere = this.sceneManager.sphereManager.getSphere(sphereName);
166
+
167
+ if (sphere) {
168
+ const scale = 1 + activation * 0.5;
169
+ sphere.scale.set(scale, scale, scale);
170
+ }
171
+ }
172
+
173
+ static weightToColor(weight) {
174
+ const positiveColor = { r: 0, g: 255, b: 0 }; // Green for positive weights
175
+ const neutralColor = { r: 255, g: 255, b: 255 }; // White for neutral weights
176
+ const negativeColor = { r: 255, g: 0, b: 0 }; // Red for negative weights
177
+
178
+ const magnitude = Math.min(1, Math.abs(weight) / 3); // Normalize weight magnitude
179
+
180
+ // Interpolate between neutral and sign-specific color
181
+ const target = weight >= 0 ? positiveColor : negativeColor;
182
+ const r = Math.round(neutralColor.r * (1 - magnitude) + target.r * magnitude);
183
+ const g = Math.round(neutralColor.g * (1 - magnitude) + target.g * magnitude);
184
+ const b = Math.round(neutralColor.b * (1 - magnitude) + target.b * magnitude);
185
+
186
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
187
+ }
188
+ }
189
+
190
+ // Expose globally for script-tag usage
191
+ window.NetworkToDisplay = NetworkToDisplay;
README.md CHANGED
@@ -1,11 +1,271 @@
1
- ---
2
- title: Simple Neural Network Visualizer
3
- emoji: 🐨
4
- colorFrom: yellow
5
- colorTo: red
6
- sdk: static
7
- pinned: false
8
- short_description: 'visualize a densely connected neural network with training. '
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Future Enhancements
2
+
3
+ - [ ] Real-time training animation via epochs -> change in colors.
4
+ - [ ] Layer-specific zoom focus
5
+ - [ ] Neuron activation heatmap
6
+ - [ ] Weight magnitude visualization
7
+ - [ ] input / output bars
8
+
9
+
10
+ # Custom Neural Network Visualizer
11
+
12
+ A 3D interactive visualization of a neural network trained on MNIST handwritten digit recognition. This project combines a custom-built neural network implementation with Three.js to create an immersive visualization of network architecture, weights, and connections.
13
+
14
+ **Purpose:** Visualization of actual model architecture is often lacking in machine learning. This project demonstrates what real-time neural network training looks like in 3D space, with dynamic weight updates and color-coded connections.
15
+
16
+ Stopped now: Entire simple model of sigmoid neural networks is in js. turns out people don't visualize this because it's a pain in the ass. so much pain.
17
+
18
+ demo:
19
+ ![Demo image](Demo.png "demonstration image on UI")
20
+
21
+
22
+ ## Features
23
+
24
+ - **Custom Neural Network**: Fully implemented backpropagation neural network from scratch in pure JavaScript (no ML frameworks)
25
+ - **MNIST Training**: Load and preprocess MNIST dataset (55,000 training samples, 10,000 test samples)
26
+ - **3D Visualization**: Interactive Three.js scene showing network architecture in 3D space
27
+ - **Input Layer Grid**: Input layer (784 neurons) displayed as a 28×28 grid (matching MNIST image dimensions)
28
+ - **Dynamic Weight Visualization**: Connections colored by weight sign and magnitude
29
+ - **Green**: Positive weights (excitatory connections)
30
+ - **Red**: Negative weights (inhibitory connections)
31
+ - **White**: Neutral/weak weights
32
+ - **Real-time Training Updates**: Weights update color dynamically as network trains
33
+ - **Smart Connection Management**: Weak weights below threshold are automatically hidden; strong weights are shown/created dynamically
34
+ - **Interactive HUD**: Top bar displays:
35
+ - Epoch counter (starts at 0)
36
+ - Mini-batch progress counter
37
+ - Real-time accuracy percentage
38
+ - Training control buttons
39
+ - **Interactive Camera**: OrbitControls for smooth 3D navigation with reset button
40
+ - **Async Training**: Non-blocking training with UI updates every 10 mini-batches
41
+
42
+ ## Project Structure
43
+
44
+ ```
45
+ ├── CustomNeuralNetwork.js # Neural network implementation with backpropagation
46
+ ├── MNIST_dataset.js # MNIST data loading from Google Cloud
47
+ ├── preprocessing.js # Data preprocessing and reshaping
48
+ ├── NetworkToDisplay.js # Converts network to 3D scene configuration
49
+ ├── SceneManager.js # Three.js scene, camera, and lighting setup
50
+ ├── SphereManager.js # Creates and manages neuron spheres
51
+ ├── ConnectionManager.js # Creates and manages weight connections
52
+ ├── main.js # Entry point and training orchestration
53
+ ├── index.html # HTML page with canvas
54
+ └── README.md # This file
55
+ ```
56
+
57
+ ## Architecture
58
+
59
+ ### Neural Network (CustomNeuralNetwork.js)
60
+
61
+ Custom implementation featuring:
62
+ - **Matrix operations**: Dot product, element-wise operations, transpose
63
+ - **Activation functions**: Sigmoid activation with derivatives
64
+ - **Training**: Stochastic Gradient Descent (SGD) with mini-batches
65
+ - **Backpropagation**: Full backpropagation algorithm for weight updates
66
+ - **Evaluation**: Accuracy testing on test data
67
+
68
+ ```javascript
69
+ // Example usage
70
+ const network = new Network([784, 16, 16, 10]); // Input, hidden, hidden, output
71
+ network.SGD(trainingData, epochs, batchSize, learningRate, testData);
72
+ ```
73
+
74
+ ### Data Pipeline (preprocessing.js)
75
+
76
+ Loads MNIST data and formats it for the network:
77
+ 1. Fetches image sprites and labels from Google Cloud Storage
78
+ 2. Reshapes flat 784-element arrays into column vectors `[[val], [val], ...]`
79
+ 3. Prepares one-hot encoded labels
80
+ 4. Returns `{trainingData, testData}` ready for training
81
+
82
+ ### Visualization (NetworkToDisplay.js)
83
+
84
+ Instance-based class that creates and updates the 3D scene directly:
85
+ - **Neurons**: Represented as spheres with layer-specific colors
86
+ - Red: Input layer (28×28 grid)
87
+ - Yellow: Hidden layers (vertical line)
88
+ - Cyan: Output layer (vertical line for 10 classes)
89
+ - **Connections**: Dynamically managed based on weight magnitude
90
+ - **Color coding by weight sign:**
91
+ - Green: Positive weights (excitatory)
92
+ - Red: Negative weights (inhibitory)
93
+ - White: Neutral weights
94
+ - **Smart visibility:** Connections below `weightThreshold` (default 0.1) are automatically hidden
95
+ - **Dynamic updates:** `updateConnections()` creates new strong connections and removes weak ones in real-time
96
+
97
+ ### 3D Scene Management (SceneManager.js)
98
+
99
+ Provides Three.js scene setup with:
100
+ - Perspective camera with orbit controls
101
+ - Ambient and directional lighting with atmospheric point lights
102
+ - Fog effect for depth perception
103
+ - Real-time rendering loop with connection position updates
104
+ - Window resize handling
105
+ - Camera reset functionality (removes momentum/velocity)
106
+
107
+ ## Usage
108
+
109
+ ### Basic Setup
110
+
111
+ 1. Open `index.html` in a modern web browser
112
+ 2. Wait for MNIST dataset to load (automatic)
113
+ 3. The initial network architecture [784, 16, 16, 10] will be visualized immediately
114
+ 4. Click **"Next Epoch"** button to train for one epoch
115
+ 5. Watch as:
116
+ - Mini-batch counter updates in real-time
117
+ - Connections change color based on weight updates
118
+ - Accuracy percentage updates after each epoch
119
+ - Epoch counter increments
120
+
121
+ ### Interactive Controls
122
+
123
+ **Mouse Controls:**
124
+ - **Left-click + drag**: Rotate view
125
+ - **Right-click + drag**: Pan camera
126
+ - **Mouse scroll**: Zoom in/out
127
+
128
+ **HUD Controls:**
129
+ - **Next Epoch button**: Train network for one epoch (async, non-blocking)
130
+ - **Reset Camera button**: Return camera to initial position and stop momentum
131
+
132
+ ### Custom Network Configuration
133
+
134
+ Edit `main.js` to modify network architecture and training parameters:
135
+
136
+ ```javascript
137
+ // Change network architecture
138
+ const network = new Network([784, 32, 32, 16, 10]);
139
+
140
+ // Adjust visualization parameters
141
+ const visualizer = new NetworkToDisplay(network, sceneManager, {
142
+ layerSpacing: 10,
143
+ neuronSpacing: 2,
144
+ neuronRadius: 0.5,
145
+ inputColor: '#ff6b6b',
146
+ hiddenColor: '#ffd93d',
147
+ outputColor: '#4ecdc4',
148
+ weightThreshold: 0.1, // Hide connections below this weight magnitude
149
+ lineWidth: 0.1,
150
+ opacity: 0.1
151
+ });
152
+
153
+ // Modify training parameters in the button click handler
154
+ network.SGD_single_epoch(trainingData, miniBatchSize=100, learningRate=0.5, testData, visualizer_function);
155
+ ```
156
+
157
+ ## Data Format
158
+
159
+ ### Training Data
160
+
161
+ Each sample is a pair `[x, y]`:
162
+ - `x`: Column vector of 784 pixel values (normalized 0-1)
163
+ ```
164
+ [[0.5], [0.3], [0.8], ...] // 784 elements
165
+ ```
166
+ - `y`: One-hot encoded label (10 elements for digits 0-9)
167
+ ```
168
+ [[0], [0], [1], [0], ...] // For digit 2
169
+ ```
170
+
171
+ ### Network Weights
172
+
173
+ - `weights[i]`: 2D array from layer i to layer i+1
174
+ - Dimensions: `[numNeuronsInLayerI+1, numNeuronsInLayerI]`
175
+ - `weights[i][j][k]`: Weight from neuron k in layer i to neuron j in layer i+1
176
+
177
+ ## Class Reference
178
+
179
+ ### Network
180
+
181
+ ```javascript
182
+ // Constructor
183
+ new Network(neuronCounts) // e.g., [784, 16, 16, 10]
184
+
185
+ // Methods
186
+ feedforward(inputVector) // Get network prediction
187
+ SGD_single_epoch(trainingData, miniBatchSize, lr, testData, cb) // Train one epoch (async)
188
+ evaluate(testData) // Count correct predictions
189
+ backpropagation(x, y) // Compute gradients
190
+ ```
191
+
192
+ ### SphereManager
193
+
194
+ ```javascript
195
+ addSphere(name, config) // Add neuron sphere
196
+ removeSphere(name) // Remove sphere
197
+ updateSphere(name, config) // Update sphere properties
198
+ getSphere(name) // Get sphere mesh
199
+ changeColorSmooth(name, color) // Animate color change
200
+ ```
201
+
202
+ ### ConnectionManager
203
+
204
+ ```javascript
205
+ addConnection(id, fromPos, toPos, config) // Add weight connection
206
+ removeConnection(id) // Remove connection
207
+ updateConnectionPositions(id, from, to) // Update endpoints
208
+ ```
209
+
210
+ ### NetworkToDisplay
211
+
212
+ ```javascript
213
+ // Constructor (instance-based, not static)
214
+ new NetworkToDisplay(network, sceneManager, options)
215
+
216
+ // Methods
217
+ initialGeneration() // Create all spheres and initial connections in scene
218
+ updateConnections() // Update/create/delete connections based on current weights
219
+ highlightNeuron(layer, neuron, activation) // Highlight active neuron (instance method)
220
+ ```
221
+
222
+ **Static Methods:**
223
+ ```javascript
224
+ NetworkToDisplay.weightToColor(weight) // Convert weight value to color (red/white/green gradient)
225
+ ```
226
+
227
+ ## Performance Considerations
228
+
229
+ - **Input layer grid**: 28×28 = 784 neurons (no sampling)
230
+ - **Dynamic connections**: Only connections with weights above threshold are rendered
231
+ - Initial: ~12,960 connections (784→16 + 16→16 + 16→10)
232
+ - Runtime: Varies based on weight magnitudes (weak connections hidden)
233
+ - **Async training**: Mini-batches update UI every 10 iterations to prevent blocking
234
+ - **Color updates**: Real-time weight-to-color conversion with interpolation
235
+ - **Rendering**: Optimized for dynamic object creation/deletion with orbit controls
236
+
237
+
238
+ ## Dependencies
239
+
240
+ - **Three.js r128**: 3D graphics library (loaded from CDN)
241
+ - **OrbitControls**: Camera control extension for Three.js (loaded from CDN)
242
+ - **TensorFlow.js 1.0.0**: Used only for MNIST data loading from Google Cloud Storage
243
+
244
+ All dependencies are loaded via CDN - no installation required.
245
+
246
+ ## Future Enhancements
247
+
248
+ - [ ] Layer-specific zoom focus
249
+ - [ ] Neuron activation heatmap (color neurons by activation value during inference)
250
+ - [ ] Input/output visualization bars showing sample images and predictions
251
+ - [ ] Weight magnitude histogram
252
+ - [ ] Training loss curve overlay
253
+ - [ ] Export/import trained model weights
254
+ - [ ] Multiple activation functions (ReLU, Tanh, etc.)
255
+ - [ ] Adjustable learning rate during training
256
+
257
+
258
+
259
+ ## References
260
+
261
+ - MNIST Dataset: [Yann LeCun's MNIST Database](http://yann.lecun.com/exdb/mnist/)
262
+ - Three.js: [Three.js Documentation](https://threejs.org/docs/)
263
+ - Neural Networks: [Neural Networks and Deep Learning](http://neuralnetworksanddeeplearning.com/)
264
+
265
+ ## License
266
+
267
+ This is an educational project for learning machine learning and 3D visualization.
268
+
269
+ ## Author
270
+
271
+ Created as part of a custom neural network learning project.
SceneManager.js ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ class SceneManager {
3
+ constructor(config = {}) {
4
+ // Scene setup
5
+ this.scene = new THREE.Scene();
6
+ this.scene.background = new THREE.Color(config.backgroundColor || 0x0a0e27);
7
+ this.scene.fog = new THREE.Fog(config.backgroundColor || 0x0a0e27, 100, 1000);
8
+
9
+ // Camera setup
10
+ this.camera = new THREE.PerspectiveCamera(
11
+ 75,
12
+ window.innerWidth / window.innerHeight,
13
+ 0.1,
14
+ 1000
15
+ );
16
+ this.camera.position.set(0, 0, config.cameraDistance || 15);
17
+
18
+ // Renderer setup
19
+ this.renderer = new THREE.WebGLRenderer({
20
+ canvas: document.getElementById('canvas'),
21
+ antialias: true,
22
+ alpha: true
23
+ });
24
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
25
+ this.renderer.setPixelRatio(window.devicePixelRatio);
26
+ this.renderer.shadowMap.enabled = true;
27
+
28
+ // Lighting
29
+ this.setupLighting();
30
+
31
+ // Managers
32
+ this.sphereManager = new SphereManager(this.scene);
33
+ this.connectionManager = new ConnectionManager(this.scene);
34
+
35
+ // Controls
36
+ this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
37
+ this.controls.enableDamping = true;
38
+ this.controls.dampingFactor = 0.05;
39
+ this.controls.screenSpacePanning = false;
40
+ this.controls.minDistance = 5;
41
+ this.controls.maxDistance = 500;
42
+ this.controls.maxPolarAngle = Math.PI;
43
+
44
+ // Store initial camera state for reset
45
+ this.initialCameraPosition = this.camera.position.clone();
46
+ this.initialControlsTarget = new THREE.Vector3(0, 0, 0);
47
+
48
+ // Window resize listener
49
+ window.addEventListener('resize', () => this.onWindowResize());
50
+
51
+ // reset camera view button
52
+ const resetCameraButton = document.getElementById('resetCameraButton');
53
+ if (resetCameraButton) {
54
+ resetCameraButton.addEventListener('click', () => {
55
+ this.resetCameraView();
56
+ });
57
+ }
58
+
59
+ // Start animation loop
60
+ this.animate();
61
+
62
+ console.log('✓ Scene3D initialized');
63
+ }
64
+
65
+ /**
66
+ * Setup lighting for the scene
67
+ */
68
+ setupLighting() {
69
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
70
+ this.scene.add(ambientLight);
71
+
72
+ // Directional light
73
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
74
+ directionalLight.position.set(10, 10, 10);
75
+ directionalLight.castShadow = true;
76
+ directionalLight.shadow.mapSize.width = 2048;
77
+ directionalLight.shadow.mapSize.height = 2048;
78
+ this.scene.add(directionalLight);
79
+
80
+ // Point lights for atmosphere
81
+ const pointLight1 = new THREE.PointLight(0xff0080, 0.3, 100);
82
+ pointLight1.position.set(-10, 10, 10);
83
+ this.scene.add(pointLight1);
84
+
85
+ const pointLight2 = new THREE.PointLight(0x0080ff, 0.3, 100);
86
+ pointLight2.position.set(10, -10, 10);
87
+ this.scene.add(pointLight2);
88
+ }
89
+
90
+
91
+
92
+ /**
93
+ * Initialize scene from JSON configuration
94
+ * @param {Object} jsonConfig - Configuration with spheres and connections arrays
95
+ *
96
+ * Example:
97
+ * {
98
+ * spheres: [
99
+ * { name: 'A', radius: 1, color: '#ff0000', position: { x: 0, y: 0, z: 0 } },
100
+ * { name: 'B', radius: 1, color: '#00ff00', position: { x: 5, y: 0, z: 0 } }
101
+ * ],
102
+ * connections: [
103
+ * { id: 'AB', from: 'A', to: 'B', color: '#ffff00', lineWidth: 2 }
104
+ * ]
105
+ * }
106
+ */
107
+ loadFromJSON(jsonConfig) {
108
+ // Clear existing scene
109
+ this.sphereManager.clearAll();
110
+ this.connectionManager.clearAll();
111
+
112
+ // Add spheres
113
+ if (jsonConfig.spheres && Array.isArray(jsonConfig.spheres)) {
114
+ jsonConfig.spheres.forEach(sphereConfig => {
115
+ const position = new THREE.Vector3(
116
+ sphereConfig.position?.x || 0,
117
+ sphereConfig.position?.y || 0,
118
+ sphereConfig.position?.z || 0
119
+ );
120
+
121
+ this.sphereManager.addSphere(sphereConfig.name, {
122
+ radius: sphereConfig.radius || 1,
123
+ color: sphereConfig.color || '#ff0000',
124
+ position: position,
125
+ opacity: sphereConfig.opacity || 1
126
+ });
127
+ });
128
+ }
129
+
130
+ // Add connections
131
+ if (jsonConfig.connections && Array.isArray(jsonConfig.connections)) {
132
+ jsonConfig.connections.forEach(connConfig => {
133
+ const fromSphere = this.sphereManager.getSphere(connConfig.from);
134
+ const toSphere = this.sphereManager.getSphere(connConfig.to);
135
+
136
+ if (fromSphere && toSphere) {
137
+ this.connectionManager.addConnection(
138
+ connConfig.id || `conn_${connConfig.from}_${connConfig.to}`,
139
+ fromSphere.position,
140
+ toSphere.position,
141
+ {
142
+ color: connConfig.color || '#00ff00',
143
+ lineWidth: connConfig.lineWidth || 2,
144
+ fromSphere: connConfig.from,
145
+ toSphere: connConfig.to
146
+ }
147
+ );
148
+ } else {
149
+ console.warn(`Connection ${connConfig.id}: sphere(s) not found`);
150
+ }
151
+ });
152
+ }
153
+
154
+ console.log('✓ Scene loaded from JSON');
155
+ }
156
+
157
+ /**
158
+ * Handle window resize
159
+ */
160
+ onWindowResize() {
161
+ this.camera.aspect = window.innerWidth / window.innerHeight;
162
+ this.camera.updateProjectionMatrix();
163
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
164
+ }
165
+
166
+ resetCameraView() {
167
+ // Disable damping temporarily to stop momentum
168
+ const wasDamping = this.controls.enableDamping;
169
+ this.controls.enableDamping = false;
170
+
171
+ // Reset to initial positions
172
+ this.camera.position.copy(this.initialCameraPosition);
173
+ this.controls.target.copy(this.initialControlsTarget);
174
+
175
+ // Reset OrbitControls internal state (removes any stored rotations/pans)
176
+ this.controls.reset();
177
+
178
+ // Force immediate update without damping
179
+ this.controls.update();
180
+
181
+ // Re-enable damping
182
+ this.controls.enableDamping = wasDamping;
183
+
184
+ console.log('✓ Camera view reset');
185
+ }
186
+
187
+ /**
188
+ * Main animation loop
189
+ */
190
+ animate() {
191
+ requestAnimationFrame(() => this.animate());
192
+
193
+ // Update controls
194
+ this.controls.update();
195
+
196
+ // Update connection positions dynamically
197
+ this.connectionManager.getAllConnections().forEach(conn => {
198
+ const fromSphere = this.sphereManager.getSphere(conn.fromSphere);
199
+ const toSphere = this.sphereManager.getSphere(conn.toSphere);
200
+
201
+ if (fromSphere && toSphere) {
202
+ this.connectionManager.updateConnectionPositions(
203
+ conn.id,
204
+ fromSphere.position,
205
+ toSphere.position
206
+ );
207
+ }
208
+ });
209
+
210
+ this.renderer.render(this.scene, this.camera);
211
+ }
212
+ }
SphereManager.js ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * SphereManager.js
3
+ * Manages the creation, deletion, and customization of spheres in the Three.js scene
4
+ */
5
+
6
+ class SphereManager {
7
+ constructor(scene) {
8
+ this.scene = scene;
9
+ this.spheres = new Map();
10
+ this.sphereMeshes = new Map();
11
+ this.geometries = new Map();
12
+ this.materials = new Map();
13
+ }
14
+
15
+ /**
16
+ * Create and add a new sphere to the scene
17
+ * @param {string} name - Unique identifier for the sphere
18
+ * @param {object} config - Configuration object
19
+ * @param {number} config.radius - Sphere radius (default: 1)
20
+ * @param {string} config.color - Hex color (default: '#ff0000')
21
+ * @param {THREE.Vector3} config.position - Position vector (default: 0,0,0)
22
+ * @param {number} config.opacity - Opacity 0-1 (default: 1)
23
+ * @returns {THREE.Mesh} The created sphere mesh
24
+ */
25
+ addSphere(name, config = {}) {
26
+ // Prevent duplicate names
27
+ if (this.spheres.has(name)) {
28
+ console.warn(`Sphere with name "${name}" already exists. Skipping.`);
29
+ return null;
30
+ }
31
+
32
+ // Set defaults
33
+ const radius = config.radius || 1;
34
+ const color = config.color || '#ff0000';
35
+ const position = config.position || new THREE.Vector3(0, 0, 0);
36
+ const opacity = config.opacity !== undefined ? config.opacity : 1;
37
+
38
+ // Create geometry and material
39
+ const geometry = new THREE.SphereGeometry(radius, 32, 32);
40
+ const material = new THREE.MeshStandardMaterial({
41
+ color: new THREE.Color(color),
42
+ opacity: opacity,
43
+ transparent: opacity < 1,
44
+ emissive: new THREE.Color(color),
45
+ emissiveIntensity: 0.3,
46
+ metalness: 0.3,
47
+ roughness: 0.4
48
+ });
49
+
50
+ // Create mesh
51
+ const mesh = new THREE.Mesh(geometry, material);
52
+ mesh.position.copy(position);
53
+ mesh.name = name;
54
+ mesh.userData = {
55
+ sphereName: name,
56
+ radius: radius,
57
+ originalColor: color,
58
+ connections: []
59
+ };
60
+
61
+ // Store references
62
+ this.scene.add(mesh);
63
+ this.spheres.set(name, mesh.userData);
64
+ this.sphereMeshes.set(name, mesh);
65
+ this.geometries.set(name, geometry);
66
+ this.materials.set(name, material);
67
+
68
+ console.log(`✓ Sphere added: ${name}`, config);
69
+ return mesh;
70
+ }
71
+
72
+ /**
73
+ * Remove a sphere from the scene
74
+ * @param {string} name - Name of the sphere to remove
75
+ */
76
+ removeSphere(name) {
77
+ const mesh = this.sphereMeshes.get(name);
78
+ if (!mesh) {
79
+ console.warn(`Sphere "${name}" not found.`);
80
+ return;
81
+ }
82
+
83
+ // Remove from scene
84
+ this.scene.remove(mesh);
85
+
86
+ // Clean up geometry and material
87
+ const geometry = this.geometries.get(name);
88
+ const material = this.materials.get(name);
89
+ if (geometry) geometry.dispose();
90
+ if (material) material.dispose();
91
+
92
+ // Remove from maps
93
+ this.spheres.delete(name);
94
+ this.sphereMeshes.delete(name);
95
+ this.geometries.delete(name);
96
+ this.materials.delete(name);
97
+
98
+ console.log(`✓ Sphere removed: ${name}`);
99
+ }
100
+
101
+ /**
102
+ * Update sphere properties
103
+ * @param {string} name - Name of the sphere
104
+ * @param {object} config - Properties to update
105
+ */
106
+ updateSphere(name, config = {}) {
107
+ const mesh = this.sphereMeshes.get(name);
108
+ if (!mesh) {
109
+ console.warn(`Sphere "${name}" not found.`);
110
+ return;
111
+ }
112
+
113
+ // Update position
114
+ if (config.position) {
115
+ mesh.position.copy(config.position);
116
+ }
117
+
118
+ // Update color
119
+ if (config.color) {
120
+ const colorObj = new THREE.Color(config.color);
121
+ mesh.material.color.copy(colorObj);
122
+ mesh.material.emissive.copy(colorObj);
123
+ mesh.userData.originalColor = config.color;
124
+ }
125
+
126
+ // Update opacity
127
+ if (config.opacity !== undefined) {
128
+ mesh.material.opacity = config.opacity;
129
+ mesh.material.transparent = config.opacity < 1;
130
+ }
131
+
132
+ // Update scale (for size changes without recreating geometry)
133
+ if (config.scale !== undefined) {
134
+ mesh.scale.set(config.scale, config.scale, config.scale);
135
+ }
136
+
137
+ console.log(`✓ Sphere updated: ${name}`);
138
+ }
139
+
140
+ /**
141
+ * Get sphere mesh by name
142
+ * @param {string} name - Name of the sphere
143
+ * @returns {THREE.Mesh} The sphere mesh
144
+ */
145
+ getSphere(name) {
146
+ return this.sphereMeshes.get(name);
147
+ }
148
+
149
+ /**
150
+ * Get all spheres
151
+ * @returns {Array} Array of sphere data
152
+ */
153
+ getAllSpheres() {
154
+ return Array.from(this.spheres.entries()).map(([name, data]) => ({
155
+ name,
156
+ ...data,
157
+ mesh: this.sphereMeshes.get(name)
158
+ }));
159
+ }
160
+
161
+ /**
162
+ * Change sphere color with animation
163
+ * @param {string} name - Name of the sphere
164
+ * @param {string} newColor - New hex color
165
+ * @param {number} duration - Animation duration in ms
166
+ */
167
+ changeColorSmooth(name, newColor, duration = 500) {
168
+ const mesh = this.sphereMeshes.get(name);
169
+ if (!mesh) return;
170
+
171
+ const startColor = mesh.material.color.getHex();
172
+ const endColor = new THREE.Color(newColor).getHex();
173
+ const startTime = Date.now();
174
+
175
+ const animate = () => {
176
+ const elapsed = Date.now() - startTime;
177
+ const progress = Math.min(elapsed / duration, 1);
178
+
179
+ const currentColor = new THREE.Color().lerpHexColors(startColor, endColor, progress);
180
+ mesh.material.color.copy(currentColor);
181
+ mesh.material.emissive.copy(currentColor);
182
+
183
+ if (progress < 1) {
184
+ requestAnimationFrame(animate);
185
+ } else {
186
+ mesh.userData.originalColor = newColor;
187
+ }
188
+ };
189
+
190
+ animate();
191
+ }
192
+
193
+ /**
194
+ * Add a glow effect to a sphere
195
+ * @param {string} name - Name of the sphere
196
+ * @param {boolean} enable - Whether to enable glow
197
+ */
198
+ setGlow(name, enable = true) {
199
+ const mesh = this.sphereMeshes.get(name);
200
+ if (!mesh) return;
201
+
202
+ if (enable) {
203
+ mesh.material.emissiveIntensity = 0.8;
204
+ } else {
205
+ mesh.material.emissiveIntensity = 0.3;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Clear all spheres
211
+ */
212
+ clearAll() {
213
+ const names = Array.from(this.spheres.keys());
214
+ names.forEach(name => this.removeSphere(name));
215
+ console.log('✓ All spheres cleared');
216
+ }
217
+
218
+ /**
219
+ * Get sphere count
220
+ * @returns {number}
221
+ */
222
+ getCount() {
223
+ return this.spheres.size;
224
+ }
225
+ }
index.html CHANGED
@@ -1,19 +1,194 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Three.js Scene</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background-color: #1a1a1a;
17
+ color: #fff;
18
+ overflow: hidden;
19
+ }
20
+
21
+ #canvas {
22
+ display: block;
23
+ width: 100%;
24
+ height: 100vh;
25
+ }
26
+
27
+ .controls-info {
28
+ position: absolute;
29
+ top: 20px;
30
+ right: 20px;
31
+ background-color: rgba(0, 0, 0, 0.8);
32
+ padding: 15px;
33
+ border-radius: 10px;
34
+ border: 2px solid #00d4ff;
35
+ font-size: 12px;
36
+ max-width: 250px;
37
+ }
38
+
39
+ .controls-info h3 {
40
+ color: #00d4ff;
41
+ margin-bottom: 10px;
42
+ }
43
+
44
+ .control-item {
45
+ margin-bottom: 8px;
46
+ }
47
+
48
+ .control-item strong {
49
+ color: #00d4ff;
50
+ }
51
+
52
+ /* HUD Top Bar */
53
+ .hud-container {
54
+ position: fixed;
55
+ top: 0;
56
+ left: 0;
57
+ right: 0;
58
+ height: 60px;
59
+ background: linear-gradient(135deg, rgba(10, 14, 39, 0.95) 0%, rgba(20, 25, 50, 0.95) 100%);
60
+ border-bottom: 2px solid #00d4ff;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: space-between;
64
+ padding: 0 30px;
65
+ box-shadow: 0 4px 20px rgba(0, 212, 255, 0.2);
66
+ z-index: 1000;
67
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
68
+ }
69
+
70
+ .hud-left {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 20px;
74
+ }
75
+
76
+ .hud-title {
77
+ font-size: 16px;
78
+ font-weight: bold;
79
+ color: #00d4ff;
80
+ letter-spacing: 1px;
81
+ }
82
+
83
+ .epoch-button {
84
+ background: linear-gradient(135deg, #00d4ff 0%, #0099cc 100%);
85
+ border: none;
86
+ color: #0a0e27;
87
+ padding: 8px 20px;
88
+ font-size: 14px;
89
+ font-weight: bold;
90
+ border-radius: 5px;
91
+ cursor: pointer;
92
+ transition: all 0.3s ease;
93
+ box-shadow: 0 4px 15px rgba(0, 212, 255, 0.4);
94
+ }
95
+
96
+ .epoch-button:hover {
97
+ transform: translateY(-2px);
98
+ box-shadow: 0 6px 20px rgba(0, 212, 255, 0.6);
99
+ }
100
+
101
+ .epoch-button:active {
102
+ transform: translateY(0);
103
+ box-shadow: 0 2px 10px rgba(0, 212, 255, 0.4);
104
+ }
105
+
106
+ .hud-right {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 10px;
110
+ }
111
+
112
+ .accuracy-label {
113
+ font-size: 14px;
114
+ color: #ffd93d;
115
+ font-weight: 600;
116
+ }
117
+
118
+ .accuracy-value {
119
+ font-size: 16px;
120
+ color: #4ecdc4;
121
+ font-weight: bold;
122
+ min-width: 60px;
123
+ text-align: right;
124
+ }
125
+
126
+ #canvas {
127
+ display: block;
128
+ width: 100%;
129
+ height: 100vh;
130
+ margin-top: 0;
131
+ }
132
+ </style>
133
+ </head>
134
+ <body>
135
+ <canvas id="canvas"></canvas>
136
+
137
+ <!-- HUD Top Bar -->
138
+ <div class="hud-container">
139
+ <div class="hud-left">
140
+ <div class="hud-title">Neural Network Visualizer</div>
141
+ <button class="epoch-button" id="epochButton">Next Epoch</button>
142
+ <!-- epoch counter: -->
143
+ <div class="epoch-counter" id="epochCounter" style="color: #ffffff; font-weight: bold; margin-left: 15px;">Epoch: 0</div>
144
+
145
+ <!-- loading / minibatch counter: -->
146
+ <div class="minibatch-counter" id="minibatchCounter" style="color: #ffffff; font-weight: bold; margin-left: 15px;">Minibatch: 0</div>
147
+
148
+ <!-- reset camera view button-->
149
+ <button class="button" id="resetCameraButton" style="margin-left: 15px; background: linear-gradient(135deg, #ff6f61 0%, #d64541 100%);">Reset Camera</button>
150
+
151
+ <a href="https://github.com/richardhcli/neural_network_visualization" target="_blank" style="margin-left: 15px; color: #00d4ff; text-decoration: none; font-weight: bold;">GitHub Repo</a>
152
+ </div>
153
+
154
+ <div class="hud-right">
155
+ <span class="accuracy-label">Accuracy:</span>
156
+ <span class="accuracy-value" id="accuracyValue">--</span>
157
+ </div>
158
+ </div>
159
+
160
+ <div class="controls-info">
161
+ <h3>Controls</h3>
162
+ <div class="control-item">
163
+ <strong>Left Click + Drag:</strong> Rotate
164
+ </div>
165
+ <div class="control-item">
166
+ <strong>Right Click + Drag:</strong> Pan
167
+ </div>
168
+ <div class="control-item">
169
+ <strong>Scroll Wheel:</strong> Zoom
170
+ </div>
171
+ <div class="control-item">
172
+ <strong>Double Click:</strong> Auto Focus
173
+ </div>
174
+ </div>
175
+
176
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
177
+ <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
178
+
179
+ <!-- Import TensorFlow.js -->
180
+ <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>
181
+
182
+
183
+
184
+ <script src="SphereManager.js"></script>
185
+ <script src="ConnectionManager.js"></script>
186
+ <script src="MNIST_dataset.js"></script>
187
+ <script src="preprocessing.js"></script>
188
+ <script src="CustomNeuralNetwork.js"></script>
189
+
190
+ <script src="NetworkToDisplay.js"></script>
191
+ <script src="SceneManager.js"></script>
192
+ <script src="main.js"></script>
193
+ </body>
194
  </html>
main.js ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * main.js
3
+ * Minimalistic Three.js scene with JSON-based initialization
4
+ */
5
+
6
+
7
+ // Initialize when DOM is loaded
8
+ document.addEventListener('DOMContentLoaded', () => {
9
+ (async () => {
10
+ window.sceneManager = new SceneManager();
11
+
12
+ const figureOptions = {
13
+ layerSpacing: 10,
14
+ neuronSpacing: 2,
15
+ neuronRadius: 0.5,
16
+ inputColor: '#ff6b6b',
17
+ hiddenColor: '#ffd93d',
18
+ outputColor: '#4ecdc4',
19
+ weightThreshold: 0.1,
20
+ lineWidth: 0.1,
21
+ opacity: 0.1
22
+ };
23
+
24
+
25
+ // preprocess MNIST data
26
+ const { trainingData, testData } = await preprocessMNISTData();
27
+ console.log('✓ MNIST data preprocessed');
28
+
29
+ //model:
30
+
31
+ //create model network:
32
+ const network = new Network([784, 16, 16, 10]); // example architecture
33
+
34
+
35
+ // Train the network
36
+ //network.SGD(trainingData, 1, 10, 0.5, testData);
37
+
38
+ //testing:
39
+ // const network = new Network([784, 1, 1, 1]);
40
+
41
+ // Visualizer instance (non-static class)
42
+ const visualizer = new NetworkToDisplay(network, sceneManager, figureOptions);
43
+
44
+ // Build spheres and initial connections directly in the scene
45
+ visualizer.initialGeneration();
46
+
47
+ const visualizer_function = () => {
48
+ visualizer.updateConnections();
49
+ console.log('✓ Visualization updated');
50
+ };
51
+
52
+ // Initialize epoch counter
53
+ let epochCount = 0;
54
+
55
+ //attach epoch training to button
56
+ const trainEpochButton = document.getElementById('epochButton');
57
+ if (trainEpochButton) {
58
+ trainEpochButton.addEventListener('click', () => {
59
+ network.SGD_single_epoch(trainingData, 100, 0.5, testData, visualizer_function);
60
+
61
+ // Increment and update epoch counter display
62
+ epochCount++;
63
+ const epochCounterDiv = document.getElementById('epochCounter');
64
+ if (epochCounterDiv) {
65
+ epochCounterDiv.textContent = `Epoch: ${epochCount}`;
66
+ }
67
+ });
68
+ }
69
+
70
+
71
+ })();
72
+
73
+ // sceneManager.loadFromJSON({
74
+ // spheres: [
75
+ // { name: 'A', radius: 1.5, color: '#ff0000', position: { x: -5, y: 0, z: 0 } },
76
+ // { name: 'B', radius: 1.5, color: '#00ff00', position: { x: 5, y: 0, z: 0 } }
77
+ // ],
78
+ // connections: [
79
+ // { id: 'AB', from: 'A', to: 'B', color: '#ffffff', lineWidth: 2 }
80
+ // ]
81
+ // });
82
+
83
+ // Example: Uncomment to add initial demo spheres
84
+ // sceneManager.addMultipleSpheres(5);
85
+ // sceneManager.connectSphereChain(['Sphere_0', 'Sphere_1', 'Sphere_2', 'Sphere_3', 'Sphere_4']);
86
+ });
preprocessing.js ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // import { MnistData } from './MNIST_dataset.js';
2
+
3
+
4
+ /**
5
+ * Loads and preprocesses MNIST dataset for neural network training
6
+ * Reshapes flat image arrays into column vectors and formats labels
7
+ * @returns {Promise<{trainingData: Array, testData: Array}>} Preprocessed training and test data
8
+ */
9
+ async function preprocessMNISTData() {
10
+
11
+ const IMAGE_SIZE = 784;
12
+ const NUM_CLASSES = 10;
13
+ const NUM_TRAIN_ELEMENTS = 55000;
14
+ const NUM_TEST_ELEMENTS = 10000;
15
+
16
+
17
+ // Load MNIST data from remote sources
18
+ const mnistData = new MnistData();
19
+ await mnistData.load();
20
+
21
+ // Convert training data to format expected by Network class
22
+ const trainingData = [];
23
+ for (let i = 0; i < NUM_TRAIN_ELEMENTS; i++) {
24
+ const imageStart = i * IMAGE_SIZE;
25
+ const labelStart = i * NUM_CLASSES;
26
+
27
+ // Reshape image from flat array into column vector [[val], [val], ...]
28
+ const x = [];
29
+ for (let j = 0; j < IMAGE_SIZE; j++) {
30
+ x.push([mnistData.trainImages[imageStart + j]]);
31
+ }
32
+
33
+ // Extract one-hot encoded label as column vector
34
+ const y = [];
35
+ for (let j = 0; j < NUM_CLASSES; j++) {
36
+ y.push([mnistData.trainLabels[labelStart + j]]);
37
+ }
38
+
39
+ trainingData.push([x, y]);
40
+ }
41
+
42
+ // Convert test data to format expected by Network class
43
+ const testData = [];
44
+ for (let i = 0; i < NUM_TEST_ELEMENTS; i++) {
45
+ const imageStart = i * IMAGE_SIZE;
46
+ const labelStart = i * NUM_CLASSES;
47
+
48
+ // Reshape image from flat array into column vector [[val], [val], ...]
49
+ const x = [];
50
+ for (let j = 0; j < IMAGE_SIZE; j++) {
51
+ x.push([mnistData.testImages[imageStart + j]]);
52
+ }
53
+
54
+ // Extract one-hot encoded label as column vector
55
+ const y = [];
56
+ for (let j = 0; j < NUM_CLASSES; j++) {
57
+ y.push([mnistData.testLabels[labelStart + j]]);
58
+ }
59
+
60
+ testData.push([x, y]);
61
+ }
62
+
63
+ return { trainingData, testData };
64
+ }
65
+
style.css DELETED
@@ -1,28 +0,0 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }