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- ConnectionManager.js +230 -0
- CustomNeuralNetwork.js +234 -0
- LICENSE +21 -0
- MNIST_dataset.js +142 -0
- NetworkToDisplay.js +191 -0
- README.md +271 -11
- SceneManager.js +212 -0
- SphereManager.js +225 -0
- index.html +193 -18
- main.js +86 -0
- preprocessing.js +65 -0
- 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+

|
| 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 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|