appvoid commited on
Commit
bf15f41
·
verified ·
1 Parent(s): d5a9bbf

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +404 -791
index.html CHANGED
@@ -1,840 +1,453 @@
1
  <!DOCTYPE html>
2
- <html>
3
-
4
  <head>
5
- <title>Carbono UI - Enhanced</title>
 
 
6
  <style>
7
- :root {
8
- --primary-color: #fff;
9
- --secondary-color: #000;
10
- --tertiary-color: #777;
11
- --background-color: #000;
12
- --widget-background: #111;
13
- --border-color: #333;
14
- --input-background: #222;
15
- --input-focus-background: #333;
16
- --button-hover-background: #000;
17
- --font-family: monospace;
18
- }
19
-
20
- a {
21
- color: var(--primary-color);
22
- }
23
-
24
- body {
25
- background: var(--background-color);
26
- color: var(--primary-color);
27
- font-family: var(--font-family);
28
- margin: 0;
29
- padding: 2%;
30
- display: flex;
31
- flex-direction: column;
32
- gap: 15px;
33
- }
34
-
35
- h3 {
36
- margin: 1rem 0;
37
- }
38
-
39
- p {
40
- margin: 0 0 1rem 0;
41
- color: var(--tertiary-color);
42
- line-height: 1.5;
43
- }
44
-
45
- .grid {
46
- display: grid;
47
- grid-template-columns: 1fr;
48
- gap: 15px;
49
- }
50
-
51
- @media (min-width: 768px) {
52
- .grid {
53
- grid-template-columns: repeat(2, 1fr);
54
- }
55
- }
56
-
57
- .widget {
58
- background: var(--widget-background);
59
- border-radius: 10px;
60
- padding: 20px;
61
- box-sizing: border-box;
62
- }
63
-
64
- .widget-title {
65
- font-size: 1.2em;
66
- margin-bottom: 15px;
67
- border-bottom: 1px solid var(--border-color);
68
- padding-bottom: 10px;
69
- }
70
-
71
- .input-group {
72
- margin-bottom: 15px;
73
- }
74
-
75
- .settings-grid {
76
- display: grid;
77
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
78
- gap: 15px;
79
- margin-bottom: 15px;
80
- }
81
-
82
- input[type="text"],
83
- input[type="number"],
84
- select,
85
- textarea {
86
- outline: none;
87
- width: 100%;
88
- padding: 8px;
89
- background: var(--input-background);
90
- border: 1px solid var(--border-color);
91
- color: var(--primary-color);
92
- border-radius: 8px;
93
- margin-top: 5px;
94
- box-sizing: border-box;
95
- transition: background 0.3s, border 0.3s;
96
- }
97
-
98
- span#loadDataBtn {
99
- background-color: var(--primary-color);
100
- color: var(--secondary-color);
101
- font-weight: 600;
102
- font-size: 12px;
103
- padding: 2px 4px;
104
- border-radius: 3px;
105
- cursor: pointer;
106
- }
107
-
108
- input[type="text"]:focus,
109
- input[type="number"]:focus,
110
- select:focus,
111
- textarea:focus {
112
- background: var(--input-focus-background);
113
- border-color: var(--primary-color);
114
- }
115
-
116
- button {
117
- background: var(--primary-color);
118
- color: var(--secondary-color);
119
- border: 1px solid var(--primary-color);
120
- padding: 8px 15px;
121
- border-radius: 6px;
122
- cursor: pointer;
123
- transition: all 0.2s ease;
124
- }
125
-
126
- button:hover,
127
- button:disabled {
128
- background: var(--button-hover-background);
129
- color: var(--primary-color);
130
- }
131
-
132
- button:disabled {
133
- cursor: not-allowed;
134
- opacity: 0.7;
135
- }
136
-
137
- .progress-container {
138
- height: 180px;
139
- position: relative;
140
- border: 1px solid var(--border-color);
141
- border-radius: 8px;
142
- margin-bottom: 10px;
143
- }
144
-
145
- .graph {
146
- position: absolute;
147
- bottom: 0;
148
- width: 100%;
149
- height: 100%;
150
- }
151
-
152
- .button-group {
153
- display: flex;
154
- gap: 10px;
155
- flex-wrap: wrap;
156
- }
157
-
158
- .epoch-progress {
159
- height: 5px;
160
- background: #222;
161
- border-radius: 8px;
162
- overflow: hidden;
163
- margin-top: 10px;
164
- }
165
-
166
- .epoch-bar {
167
- height: 100%;
168
- width: 0;
169
- background: var(--primary-color);
170
- transition: width 0.3s ease;
171
  }
172
  </style>
173
  </head>
174
-
175
  <body>
176
- <h3>Playground</h3>
177
- <p>This is a web app for showcasing Carbono, a self-contained micro-library that makes it super easy to play, create and share small neural networks. To download it and know more you can go to the <a href="https://github.com/appvoid/carbono" target="_blank">GitHub repo</a>. You can see additional training details by opening the console. To load a dummy dataset, <span id="loadDataBtn">click here</span> and then click the "Train" button.</p>
 
 
 
 
178
 
179
- <div class="grid">
180
- <div class="widget">
181
- <div class="widget-title">Model Settings</div>
182
- <div class="input-group">
183
- <label>Training Set:</label>
184
- <textarea id="trainingData" rows="4" placeholder="1,1,1,0\n1,0,1,0\n0,1,0,1"></textarea>
185
- <p>Last number represents the desired output.</p>
186
- </div>
187
- <div class="input-group">
188
- <label>Validation Set:</label>
189
- <textarea id="testData" rows="3" placeholder="0,0,0,1"></textarea>
190
- </div>
191
- <div class="settings-grid">
192
- <div class="input-group">
193
- <label>Epochs:</label>
194
- <input type="number" id="epochs" value="50">
195
  </div>
196
- <div class="input-group">
197
- <label>Learning Rate:</label>
198
- <input type="number" id="learningRate" value="0.1" step="0.001">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  </div>
200
- <div class="input-group">
201
- <label>Batch Size:</label>
202
- <input type="number" id="batchSize" value="8">
 
203
  </div>
204
- <div class="input-group">
205
- <label>Hidden Layers:</label>
206
- <input type="number" id="numHiddenLayers" value="1">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  </div>
208
- </div>
209
- <div id="hiddenLayersConfig"></div>
210
- </div>
211
-
212
- <div class="widget">
213
- <div class="widget-title">Training Progress</div>
214
- <div class="progress-container">
215
- <canvas id="lossGraph" class="graph"></canvas>
216
- </div>
217
- <p>Training loss is white, validation loss is gray.</p>
218
- <div class="epoch-progress">
219
- <div id="epochBar" class="epoch-bar"></div>
220
- </div>
221
- <div id="stats" style="margin-top: 10px;"></div>
222
 
223
- <div class="widget-title" style="margin-top: 20px;">Model Management</div>
224
- <p>Save the weights to load them in your app or share them on Hugging Face!</p>
225
- <div class="button-group">
226
- <button id="trainButton">Train</button>
227
- <button id="saveButton">Save</button>
228
- <button id="loadButton">Load</button>
229
- </div>
230
-
231
- <div class="widget-title" style="margin-top: 20px;">Prediction</div>
232
- <p>Predict output.</p>
233
- <div class="input-group">
234
- <label>Input:</label>
235
- <input type="text" id="predictionInput" placeholder="0.4, 0.2, 0.6">
236
- </div>
237
- <button id="predictButton">Predict</button>
238
- <div id="predictionResult" style="margin-top: 10px;"></div>
239
 
240
- <div class="widget-title" style="margin-top: 20px;">Visualization</div>
241
- <div class="progress-container">
242
- <canvas id="networkGraph" class="graph"></canvas>
243
- </div>
244
- <p>Internal model's representation.</p>
245
  </div>
246
  </div>
247
 
248
  <script>
249
- // Carbono library code remains the same...
250
- class carbono {
251
- constructor(debug = true) {
252
- this.layers = [];
253
- this.weights = [];
254
- this.biases = [];
255
- this.activations = [];
256
- this.details = {};
257
- this.debug = debug;
258
- }
259
- // Add a new layer to the neural network
260
- layer(inputSize, outputSize, activation = 'tanh') {
261
- this.layers.push({
262
- inputSize,
263
- outputSize,
264
- activation
265
- });
266
- if (this.weights.length > 0) {
267
- const lastLayerOutputSize = this.layers[this.layers.length - 2].outputSize;
268
- if (inputSize !== lastLayerOutputSize) {
269
- throw new Error('Oops! The input size of the new layer must match the output size of the previous layer.');
270
- }
271
- }
272
- const weights = [];
273
- for (let i = 0; i < outputSize; i++) {
274
- const row = [];
275
- for (let j = 0; j < inputSize; j++) {
276
- row.push((Math.random() - 0.5) * 2 * Math.sqrt(6 / (inputSize + outputSize)));
277
- }
278
- weights.push(row);
279
- }
280
- this.weights.push(weights);
281
- const biases = Array(outputSize).fill(0.01);
282
- this.biases.push(biases);
283
- this.activations.push(activation);
284
- }
285
- // Apply the activation function
286
- activationFunction(x, activation) {
287
- switch (activation) {
288
- case 'tanh':
289
- return Math.tanh(x);
290
- case 'sigmoid':
291
- return 1 / (1 + Math.exp(-x));
292
- case 'relu':
293
- return Math.max(0, x);
294
- case 'selu':
295
- const alpha = 1.67326;
296
- const scale = 1.0507;
297
- return x > 0 ? scale * x : scale * alpha * (Math.exp(x) - 1);
298
- default:
299
- throw new Error('Whoops! We don\'t know that activation function.');
300
- }
301
- }
302
- // Calculate the derivative of the activation function
303
- activationDerivative(x, activation) {
304
- switch (activation) {
305
- case 'tanh':
306
- return 1 - Math.pow(Math.tanh(x), 2);
307
- case 'sigmoid':
308
- const sigmoid = 1 / (1 + Math.exp(-x));
309
- return sigmoid * (1 - sigmoid);
310
- case 'relu':
311
- return x > 0 ? 1 : 0;
312
- case 'selu':
313
- const alpha = 1.67326;
314
- const scale = 1.0507;
315
- return x > 0 ? scale : scale * alpha * Math.exp(x);
316
- default:
317
- throw new Error('Oops! We don\'t know the derivative of that activation function.');
318
- }
319
- }
320
- // Positional Encoding
321
- positionalEncoding(input, maxLen) {
322
- const pe = new Array(maxLen).fill(0).map((_, pos) => {
323
- return new Array(input[0].length).fill(0).map((_, i) => {
324
- const angle = pos / Math.pow(10000, 2 * i / input[0].length);
325
- return pos % 2 === 0 ? Math.sin(angle) : Math.cos(angle);
326
- });
327
- });
328
- return input.map((seq, idx) => seq.map((val, i) => val + pe[idx][i]));
329
- }
330
- // Simplified Multi-Head Self-Attention
331
- multiHeadSelfAttention(input, numHeads = 2) {
332
- const headSize = input[0].length / numHeads;
333
- const heads = new Array(numHeads).fill(0).map(() => new Array(input.length).fill(0).map(() => new Array(headSize).fill(0)));
334
- for (let h = 0; h < numHeads; h++) {
335
- for (let i = 0; i < input.length; i++) {
336
- for (let j = 0; j < headSize; j++) {
337
- heads[h][i][j] = input[i][h * headSize + j];
338
- }
339
- }
340
- }
341
- const attentionScores = new Array(numHeads).fill(0).map(() => new Array(input.length).fill(0).map(() => new Array(input.length).fill(0)));
342
- for (let h = 0; h < numHeads; h++) {
343
- for (let i = 0; i < input.length; i++) {
344
- for (let j = 0; j < input.length; j++) {
345
- let score = 0;
346
- for (let k = 0; k < headSize; k++) {
347
- score += heads[h][i][k] * heads[h][j][k];
348
- }
349
- attentionScores[h][i][j] = score;
350
- }
351
- }
352
- }
353
- const attentionWeights = attentionScores.map(head => head.map(row => row.map(score => Math.exp(score) / row.reduce((sum, s) => sum + Math.exp(s), 0))));
354
- const output = new Array(input.length).fill(0).map(() => new Array(input[0].length).fill(0));
355
- for (let h = 0; h < numHeads; h++) {
356
- for (let i = 0; i < input.length; i++) {
357
- for (let j = 0; j < headSize; j++) {
358
- for (let k = 0; k < input.length; k++) {
359
- output[i][h * headSize + j] += attentionWeights[h][i][k] * heads[h][k][j];
360
- }
361
- }
362
- }
363
- }
364
- return output;
365
  }
366
- // Layer Normalization
367
- layerNormalization(input) {
368
- const mean = input.reduce((sum, val) => sum + val, 0) / input.length;
369
- const variance = input.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / input.length;
370
- return input.map(val => (val - mean) / Math.sqrt(variance + 1e-5));
 
 
 
 
 
 
 
371
  }
372
- // Train the neural network
373
- async train(trainSet, options = {}) {
374
- const {
375
- epochs = 200,
376
- learningRate = 0.212,
377
- batchSize = 16,
378
- printEveryEpochs = 100,
379
- earlyStopThreshold = 1e-6,
380
- testSet = null,
381
- callback = null
382
- } = options;
383
- const start = Date.now();
384
- if (batchSize < 1) batchSize = 2;
385
- if (this.layers.length === 0) {
386
- const numInputs = trainSet[0].input.length;
387
- this.layer(numInputs, numInputs, 'tanh');
388
- this.layer(numInputs, 1, 'tanh');
389
- }
390
- let lastTrainLoss = 0;
391
- let lastTestLoss = null;
392
- for (let epoch = 0; epoch < epochs; epoch++) {
393
- let trainError = 0;
394
- for (let b = 0; b < trainSet.length; b += batchSize) {
395
- const batch = trainSet.slice(b, b + batchSize);
396
- let batchError = 0;
397
- for (const data of batch) {
398
- const layerInputs = [data.input];
399
- for (let i = 0; i < this.weights.length; i++) {
400
- const inputs = layerInputs[i];
401
- const weights = this.weights[i];
402
- const biases = this.biases[i];
403
- const activation = this.activations[i];
404
- const outputs = [];
405
- for (let j = 0; j < weights.length; j++) {
406
- const weight = weights[j];
407
- let sum = biases[j];
408
- for (let k = 0; k < inputs.length; k++) {
409
- sum += inputs[k] * weight[k];
410
- }
411
- outputs.push(this.activationFunction(sum, activation));
412
- }
413
- layerInputs.push(outputs);
414
- }
415
- const outputLayerIndex = this.weights.length - 1;
416
- const outputLayerInputs = layerInputs[layerInputs.length - 1];
417
- const outputErrors = [];
418
- for (let i = 0; i < outputLayerInputs.length; i++) {
419
- const error = data.output[i] - outputLayerInputs[i];
420
- outputErrors.push(error);
421
  }
422
- let layerErrors = [outputErrors];
423
- for (let i = this.weights.length - 2; i >= 0; i--) {
424
- const nextLayerWeights = this.weights[i + 1];
425
- const nextLayerErrors = layerErrors[0];
426
- const currentLayerInputs = layerInputs[i + 1];
427
- const currentActivation = this.activations[i];
428
- const errors = [];
429
- for (let j = 0; j < this.layers[i].outputSize; j++) {
430
- let error = 0;
431
- for (let k = 0; k < this.layers[i + 1].outputSize; k++) {
432
- error += nextLayerErrors[k] * nextLayerWeights[k][j];
433
- }
434
- errors.push(error * this.activationDerivative(currentLayerInputs[j], currentActivation));
435
- }
436
- layerErrors.unshift(errors);
437
  }
438
- for (let i = 0; i < this.weights.length; i++) {
439
- const inputs = layerInputs[i];
440
- const errors = layerErrors[i];
441
- const weights = this.weights[i];
442
- const biases = this.biases[i];
443
- for (let j = 0; j < weights.length; j++) {
444
- const weight = weights[j];
445
- for (let k = 0; k < inputs.length; k++) {
446
- weight[k] += learningRate * errors[j] * inputs[k];
447
- }
448
- biases[j] += learningRate * errors[j];
449
- }
450
  }
451
- batchError += Math.abs(outputErrors[0]);
452
  }
453
- trainError += batchError;
454
  }
455
- lastTrainLoss = trainError / trainSet.length;
456
- if (testSet) {
457
- let testError = 0;
458
- for (const data of testSet) {
459
- const prediction = this.predict(data.input);
460
- testError += Math.abs(data.output[0] - prediction[0]);
461
- }
462
- lastTestLoss = testError / testSet.length;
463
- }
464
- if ((epoch + 1) % printEveryEpochs === 0 && this.debug === true) {
465
- console.log(`Epoch ${epoch + 1}, Train Loss: ${lastTrainLoss.toFixed(6)}${testSet ? `, Test Loss: ${lastTestLoss.toFixed(6)}` : ''}`);
466
- }
467
- if (callback) {
468
- await callback(epoch + 1, lastTrainLoss, lastTestLoss);
469
- }
470
- await new Promise(resolve => setTimeout(resolve, 0));
471
- if (lastTrainLoss < earlyStopThreshold) {
472
- console.log(`We stopped at epoch ${epoch + 1} with train loss: ${lastTrainLoss.toFixed(6)}${testSet ? ` and test loss: ${lastTestLoss.toFixed(6)}` : ''}`);
473
- break;
474
- }
475
- }
476
- const end = Date.now();
477
- let totalParams = 0;
478
- for (let i = 0; i < this.weights.length; i++) {
479
- const weightLayer = this.weights[i];
480
- const biasLayer = this.biases[i];
481
- totalParams += weightLayer.flat().length + biasLayer.length;
482
- }
483
- const trainingSummary = {
484
- trainLoss: lastTrainLoss,
485
- testLoss: lastTestLoss,
486
- parameters: totalParams,
487
- training: {
488
- time: end - start,
489
- epochs,
490
- learningRate,
491
- batchSize
492
- },
493
- layers: this.layers.map(layer => ({
494
- inputSize: layer.inputSize,
495
- outputSize: layer.outputSize,
496
- activation: layer.activation
497
- }))
498
- };
499
- this.details = trainingSummary;
500
- return trainingSummary;
501
  }
502
- // Use the trained network to make predictions
503
- predict(input) {
504
- let layerInput = input;
505
- const allActivations = [input];
506
- const allRawValues = [];
507
- for (let i = 0; i < this.weights.length; i++) {
508
- const weights = this.weights[i];
509
- const biases = this.biases[i];
510
- const activation = this.activations[i];
511
- const layerOutput = [];
512
- const rawValues = [];
513
- for (let j = 0; j < weights.length; j++) {
514
- const weight = weights[j];
515
- let sum = biases[j];
516
- for (let k = 0; k < layerInput.length; k++) {
517
- sum += layerInput[k] * weight[k];
518
- }
519
- rawValues.push(sum);
520
- layerOutput.push(this.activationFunction(sum, activation));
521
- }
522
- allRawValues.push(rawValues);
523
- allActivations.push(layerOutput);
524
- layerInput = layerOutput;
525
  }
526
- this.lastActivations = allActivations;
527
- this.lastRawValues = allRawValues;
528
- return layerInput;
529
  }
530
- // Save the model to a file
531
- save(name = 'model') {
532
- const data = {
533
- weights: this.weights,
534
- biases: this.biases,
535
- activations: this.activations,
536
- layers: this.layers,
537
- details: this.details
538
- };
539
- const blob = new Blob([JSON.stringify(data)], {
540
- type: 'application/json'
541
- });
542
- const url = URL.createObjectURL(blob);
543
- const a = document.createElement('a');
544
- a.href = url;
545
- a.download = `${name}.json`;
546
- a.click();
547
- URL.revokeObjectURL(url);
548
  }
549
- // Load a saved model from a file
550
- load(callback) {
551
- const handleListener = (event) => {
552
- const file = event.target.files[0];
553
- if (!file) return;
554
- const reader = new FileReader();
555
- reader.onload = (event) => {
556
- const text = event.target.result;
557
- try {
558
- const data = JSON.parse(text);
559
- this.weights = data.weights;
560
- this.biases = data.biases;
561
- this.activations = data.activations;
562
- this.layers = data.layers;
563
- this.details = data.details;
564
- callback();
565
- if (this.debug === true) console.log('Model loaded successfully!');
566
- input.removeEventListener('change', handleListener);
567
- input.remove();
568
- } catch (e) {
569
- input.removeEventListener('change', handleListener);
570
- input.remove();
571
- if (this.debug === true) console.error('Failed to load model:', e);
572
- }
573
- };
574
- reader.readAsText(file);
575
  };
576
- const input = document.createElement('input');
577
- input.type = 'file';
578
- input.accept = '.json';
579
- input.style.opacity = '0';
580
- document.body.append(input);
581
- input.addEventListener('change', handleListener.bind(this));
582
- input.click();
583
  }
584
  }
585
 
586
- document.addEventListener('DOMContentLoaded', () => {
587
- const nn = new carbono();
588
- let lossHistory = [];
589
-
590
- const lossCanvas = document.getElementById('lossGraph');
591
- const networkCanvas = document.getElementById('networkGraph');
592
- const lossCtx = lossCanvas.getContext('2d');
593
-
594
- const elements = {
595
- loadDataBtn: document.getElementById('loadDataBtn'),
596
- trainingData: document.getElementById('trainingData'),
597
- testData: document.getElementById('testData'),
598
- numHiddenLayers: document.getElementById('numHiddenLayers'),
599
- hiddenLayersConfig: document.getElementById('hiddenLayersConfig'),
600
- trainButton: document.getElementById('trainButton'),
601
- stats: document.getElementById('stats'),
602
- epochBar: document.getElementById('epochBar'),
603
- epochs: document.getElementById('epochs'),
604
- learningRate: document.getElementById('learningRate'),
605
- batchSize: document.getElementById('batchSize'),
606
- predictButton: document.getElementById('predictButton'),
607
- predictionInput: document.getElementById('predictionInput'),
608
- predictionResult: document.getElementById('predictionResult'),
609
- saveButton: document.getElementById('saveButton'),
610
- loadButton: document.getElementById('loadButton')
611
- };
612
-
613
- const parseCSV = (csv) => {
614
- return csv.trim().split('\n').map(row => {
615
- const values = row.split(',').map(Number);
616
- return {
617
- input: values.slice(0, -1),
618
- output: [values[values.length - 1]]
619
- };
620
- });
621
- }
622
-
623
- const drawLossGraph = () => {
624
- const {
625
- width,
626
- height
627
- } = lossCanvas;
628
- lossCtx.clearRect(0, 0, width, height);
629
-
630
- const maxLoss = Math.max(...lossHistory.map(loss => Math.max(loss.train, loss.test || 0)));
631
-
632
- const drawLine = (data, color) => {
633
- lossCtx.strokeStyle = color;
634
- lossCtx.beginPath();
635
- data.forEach((val, i) => {
636
- const x = (i / (data.length - 1)) * width;
637
- const y = height - (val / maxLoss) * height;
638
- if (i === 0) lossCtx.moveTo(x, y);
639
- else lossCtx.lineTo(x, y);
640
- });
641
- lossCtx.stroke();
642
- };
643
-
644
- drawLine(lossHistory.map(l => l.train), 'white');
645
- if (lossHistory.some(l => l.test !== undefined)) {
646
- drawLine(lossHistory.map(l => l.test), '#777');
647
- }
648
- }
649
-
650
- const createLayerConfigUI = (numLayers) => {
651
- elements.hiddenLayersConfig.innerHTML = '';
652
- for (let i = 0; i < numLayers; i++) {
653
- const group = document.createElement('div');
654
- group.className = 'input-group settings-grid';
655
- group.innerHTML = `
656
- <div>
657
- <label>Layer ${i + 1} Nodes:</label>
658
- <input type="number" value="5" data-layer-index="${i}">
659
- </div>
660
- <div>
661
- <label>Activation:</label>
662
- <select data-layer-index="${i}">
663
- <option>tanh</option>
664
- <option>sigmoid</option>
665
- <option>relu</option>
666
- <option>selu</option>
667
- </select>
668
- </div>
669
- `;
670
- elements.hiddenLayersConfig.appendChild(group);
671
- }
672
- }
673
-
674
- const trainModel = async () => {
675
- lossHistory = [];
676
- const trainingData = parseCSV(elements.trainingData.value);
677
- const testData = parseCSV(elements.testData.value);
678
-
679
- elements.stats.innerHTML = '';
680
- const numHiddenLayers = parseInt(elements.numHiddenLayers.value);
681
- const layerConfigs = [];
682
-
683
- for (let i = 0; i < numHiddenLayers; i++) {
684
- const sizeInput = document.querySelector(`input[data-layer-index="${i}"]`);
685
- const activationSelect = document.querySelector(`select[data-layer-index="${i}"]`);
686
- layerConfigs.push({
687
- size: parseInt(sizeInput.value),
688
- activation: activationSelect.value
689
- });
690
- }
691
-
692
- nn.layers = [];
693
- nn.weights = [];
694
- nn.biases = [];
695
- nn.activations = [];
696
-
697
- const numInputs = trainingData[0].input.length;
698
- nn.layer(numInputs, layerConfigs[0].size, layerConfigs[0].activation);
699
- for (let i = 1; i < layerConfigs.length; i++) {
700
- nn.layer(layerConfigs[i - 1].size, layerConfigs[i].size, layerConfigs[i].activation);
701
- }
702
- nn.layer(layerConfigs[layerConfigs.length - 1].size, 1, 'tanh');
703
 
704
- const options = {
705
- epochs: parseInt(elements.epochs.value),
706
- learningRate: parseFloat(elements.learningRate.value),
707
- batchSize: parseInt(elements.batchSize.value),
708
- printEveryEpochs: 1,
709
- testSet: testData.length > 0 ? testData : null,
710
- callback: async (epoch, trainLoss, testLoss) => {
711
- lossHistory.push({
712
- train: trainLoss,
713
- test: testLoss
714
- });
715
- drawLossGraph();
716
- elements.epochBar.style.width = `${(epoch / options.epochs) * 100}%`;
717
- elements.stats.innerHTML = `<p>Epoch: ${epoch}/${options.epochs}<br>Train/Val Loss: ${trainLoss.toFixed(6)}${testLoss ? ` | ${testLoss.toFixed(6)}` : ''}</p>`;
718
- }
719
- }
 
 
 
 
 
 
 
720
 
721
- try {
722
- elements.trainButton.disabled = true;
723
- elements.trainButton.textContent = 'Training...';
724
- await nn.train(trainingData, options);
725
- elements.stats.innerHTML += '<strong>Model trained</strong>';
726
- } catch (error) {
727
- console.error('Training error:', error);
728
- } finally {
729
- elements.trainButton.disabled = false;
730
- elements.trainButton.textContent = 'Train';
731
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
  }
 
733
 
734
- function drawNetwork() {
735
- const ctx = networkCanvas.getContext('2d');
736
- ctx.clearRect(0, 0, networkCanvas.width, networkCanvas.height);
737
- if (!nn.lastActivations) return;
738
-
739
- const padding = 40;
740
- const width = networkCanvas.width - padding * 2;
741
- const height = networkCanvas.height - padding * 2;
742
 
743
- const layerPositions = [];
744
- const inputLayer = [];
745
- const inputX = padding;
746
- const inputSize = nn.layers[0].inputSize;
747
- for (let i = 0; i < inputSize; i++) {
748
- const inputY = padding + (inputSize > 1 ? (height * i) / (inputSize - 1) : height / 2);
749
- inputLayer.push({ x: inputX, y: inputY, value: nn.lastActivations[0][i] });
750
- }
751
- layerPositions.push(inputLayer);
752
 
753
- for (let i = 1; i < nn.lastActivations.length - 1; i++) {
754
- const layer = nn.lastActivations[i];
755
- const layerNodes = [];
756
- const layerX = padding + (width * i) / (nn.lastActivations.length - 1);
757
- for (let j = 0; j < layer.length; j++) {
758
- const nodeY = padding + (layer.length > 1 ? (height * j) / (layer.length - 1) : height / 2);
759
- layerNodes.push({ x: layerX, y: nodeY, value: layer[j] });
760
- }
761
- layerPositions.push(layerNodes);
762
- }
763
 
764
- const outputLayer = [];
765
- const outputX = networkCanvas.width - padding;
766
- const outputY = padding + height / 2;
767
- outputLayer.push({ x: outputX, y: outputY, value: nn.lastActivations[nn.lastActivations.length - 1][0] });
768
- layerPositions.push(outputLayer);
769
 
770
- ctx.lineWidth = 1;
771
- for (let i = 0; i < layerPositions.length - 1; i++) {
772
- const currentLayer = layerPositions[i];
773
- const nextLayer = layerPositions[i + 1];
774
- const weights = nn.weights[i];
775
- for (let j = 0; j < currentLayer.length; j++) {
776
- for (let k = 0; k < nextLayer.length; k++) {
777
- const weight = weights[k][j];
778
- const signal = Math.abs(currentLayer[j].value * weight);
779
- const opacity = Math.min(Math.max(signal, 0.01), 1);
780
- ctx.strokeStyle = `rgba(255, 255, 255, ${opacity})`;
781
- ctx.beginPath();
782
- ctx.moveTo(currentLayer[j].x, currentLayer[j].y);
783
- ctx.lineTo(nextLayer[k].x, nextLayer[k].y);
784
- ctx.stroke();
785
- }
786
- }
787
- }
788
 
789
- for (const layer of layerPositions) {
790
- for (const node of layer) {
791
- const value = Math.abs(node.value);
792
- const radius = 4;
793
- ctx.fillStyle = `rgba(255, 255, 255, ${Math.min(Math.max(value, 0.2), 1)})`;
794
- ctx.beginPath();
795
- ctx.arc(node.x, node.y, radius, 0, Math.PI * 2);
796
- ctx.fill();
797
- ctx.strokeStyle = 'rgba(255, 255, 255, 1.0)';
798
- ctx.lineWidth = 1;
799
- ctx.stroke();
800
- }
801
- }
802
- }
803
 
804
- function resizeCanvases() {
805
- [lossCanvas, networkCanvas].forEach(canvas => {
806
- canvas.width = canvas.parentElement.clientWidth;
807
- canvas.height = canvas.parentElement.clientHeight;
808
- });
809
- drawNetwork();
 
810
  }
 
 
 
 
 
 
 
811
 
812
- elements.loadDataBtn.onclick = () => {
813
- elements.trainingData.value = `1.0, 0.0, 0.0, 0.0\n0.7, 0.7, 0.8, 1\n0.0, 1.0, 0.0, 0.5`;
814
- elements.testData.value = `0.4, 0.2, 0.6, 1.0\n0.2, 0.82, 0.83, 1.0`;
815
- };
816
-
817
- elements.numHiddenLayers.addEventListener('change', (e) => createLayerConfigUI(parseInt(e.target.value)));
818
- elements.trainButton.addEventListener('click', trainModel);
819
- elements.predictButton.addEventListener('click', () => {
820
- const input = elements.predictionInput.value.split(',').map(Number);
821
- const prediction = nn.predict(input);
822
- elements.predictionResult.innerHTML = `Prediction: ${prediction[0].toFixed(6)}`;
823
- drawNetwork();
824
- });
825
- elements.saveButton.addEventListener('click', () => nn.save('model'));
826
- elements.loadButton.addEventListener('click', () => {
827
- nn.load(() => {
828
- elements.stats.innerHTML += '<p><strong>Model loaded successfully!</strong></p>';
829
- });
830
- });
831
-
832
- window.addEventListener('resize', resizeCanvases);
833
 
834
- createLayerConfigUI(parseInt(elements.numHiddenLayers.value));
835
- resizeCanvases();
836
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
  </script>
838
  </body>
839
-
840
  </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="en">
 
3
  <head>
4
+ <meta charset="utf-8" />
5
+ <title>Carbono UI — Minimal Mono</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
  <style>
8
+ :root{
9
+ --fg:#fff;--bg:#000;--muted:#8a8a8a;--panel:#0e0e0e;--line:#222;--ink:#111;--ink2:#1a1a1a;
10
+ --radius:6px;--pad:10px;--fs:12px;--lh:1.25;--gap:10px;
11
+ --base-w:1200; /* logical pixels for layout before scaling */
12
+ --base-h:680;
13
+ }
14
+ /* reset */
15
+ *{box-sizing:border-box;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
16
+ html,body{height:100%;background:var(--bg);color:var(--fg);margin:0}
17
+ a{color:var(--fg);text-decoration:none;border-bottom:1px solid transparent}
18
+ a:hover{border-bottom-color:var(--fg)}
19
+ /* fit-to-viewport wrapper (no scroll) */
20
+ body{display:flex;align-items:center;justify-content:center;overflow:hidden}
21
+ #fit{
22
+ width:calc(var(--base-w)*1px);
23
+ height:calc(var(--base-h)*1px);
24
+ transform-origin: center; /* <<< FIX: Was 'top left', now it scales from the center */
25
+ display:flex;flex-direction:column;gap:var(--gap);
26
+ font:500 var(--fs)/var(--lh) ui-monospace,SFMono-Regular,Menlo,Consolas,Monaco,monospace;
27
+ letter-spacing:.1px;font-variant-numeric:tabular-nums;
28
+ }
29
+ /* top bar */
30
+ .top{
31
+ display:grid;grid-template-columns:1fr auto auto;gap:var(--gap);
32
+ align-items:center;padding:6px 8px;border:1px solid var(--line);border-radius:var(--radius);background:var(--panel)
33
+ }
34
+ .brand{text-transform:uppercase;letter-spacing:.5px;font-weight:700}
35
+ .muted{color:var(--muted)}
36
+ .chip{background:var(--fg);color:var(--bg);padding:2px 6px;border-radius:999px;cursor:pointer;user-select:none}
37
+ /* layout grid */
38
+ .grid{
39
+ display:grid;gap:var(--gap);
40
+ grid-template-columns: 3.5fr 3.5fr 3fr; /* L / M / R */
41
+ grid-template-rows: 1fr;
42
+ height:100%;
43
+ }
44
+ .panel{
45
+ display:flex;flex-direction:column;gap:var(--gap);
46
+ border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(180deg,var(--panel),var(--ink));
47
+ padding:var(--pad);
48
+ }
49
+ .block{border:1px solid var(--line);border-radius:var(--radius);background:var(--ink2);padding:8px}
50
+ .title{
51
+ font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin:0 0 6px 0;
52
+ padding-bottom:6px;border-bottom:1px solid var(--line)
53
+ }
54
+ /* compact form */
55
+ label{display:block;margin-bottom:4px;color:var(--muted)}
56
+ .row{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--gap)}
57
+ .input,textarea,select{
58
+ width:100%;background:#141414;border:1px solid var(--line);color:var(--fg);
59
+ padding:6px;border-radius:4px;outline:none;transition:border-color .15s ease;
60
+ }
61
+ .input:focus,textarea:focus,select:focus{border-color:#fff}
62
+ textarea{resize:none}
63
+ /* controls */
64
+ .btns{display:flex;gap:6px;flex-wrap:wrap}
65
+ button{
66
+ height:26px;line-height:24px;padding:0 10px;border-radius:4px;border:1px solid #fff;background:#fff;color:#000;
67
+ cursor:pointer;transition:filter .15s ease
68
+ }
69
+ button:hover{filter:brightness(.9)}
70
+ button:disabled{opacity:.6;cursor:not-allowed;filter:none}
71
+ /* canvases */
72
+ .canvas-wrap{height:170px;border:1px solid var(--line);border-radius:4px;position:relative;background:#0a0a0a}
73
+ canvas{position:absolute;inset:0;width:100%;height:100%}
74
+ /* progress bar */
75
+ .bar{height:4px;border-radius:999px;background:#161616;overflow:hidden}
76
+ .bar>i{display:block;height:100%;width:0;background:#fff;transition:width .2s linear}
77
+ /* small text blocks */
78
+ .hint{color:var(--muted);margin:4px 0 0 0}
79
+ .stat{display:grid;grid-template-columns:auto 1fr;gap:6px 10px}
80
+ .stat b{color:#fff}
81
+ /* two-up prediction area */
82
+ .pred{display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center}
83
+ /* hide scrollbars anywhere just in case */
84
+ .panel,.block{overflow:hidden}
85
+ @media (max-width:900px){
86
+ /* fallback: stack but still scaled to fit by transformer */
87
+ .grid{grid-template-columns:1fr}
88
+ }
89
+ @media (prefers-reduced-motion:reduce){
90
+ *{animation:none!important;transition:none!important}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  }
92
  </style>
93
  </head>
 
94
  <body>
95
+ <div id="fit">
96
+ <div class="top">
97
+ <div class="brand">Carbono Playground</div>
98
+ <div class="muted">Learning. Simple. Visual.</div>
99
+ <span id="loadDataBtn" class="chip">Load sample</span>
100
+ </div>
101
 
102
+ <div class="grid">
103
+ <!-- LEFT: Model Settings -->
104
+ <section class="panel" aria-label="Model Settings">
105
+ <h4 class="title">Model</h4>
106
+ <div class="block">
107
+ <label>Training Set</label>
108
+ <textarea id="trainingData" rows="3" placeholder="1,1,1,0&#10;1,0,1,0&#10;0,1,0,1"></textarea>
109
+ <div class="hint">Last number is target.</div>
 
 
 
 
 
 
 
 
110
  </div>
111
+ <div class="row">
112
+ <div class="block">
113
+ <label>Epochs</label>
114
+ <input class="input" type="number" id="epochs" value="50" />
115
+ </div>
116
+ <div class="block">
117
+ <label>LR</label>
118
+ <input class="input" type="number" id="learningRate" value="0.1" step="0.001" />
119
+ </div>
120
+ <div class="block">
121
+ <label>Batch</label>
122
+ <input class="input" type="number" id="batchSize" value="8" />
123
+ </div>
124
+ <div class="block">
125
+ <label>Hidden</label>
126
+ <input class="input" type="number" id="numHiddenLayers" value="1" min="1" max="4" />
127
+ </div>
128
  </div>
129
+ <div id="hiddenLayersConfig" class="block"></div>
130
+ <div class="block">
131
+ <label>Validation Set</label>
132
+ <textarea id="testData" rows="2" placeholder="0,0,0,1"></textarea>
133
  </div>
134
+ </section>
135
+
136
+ <!-- MIDDLE: Training / Viz -->
137
+ <section class="panel" aria-label="Training & Visualization">
138
+ <h4 class="title">Training</h4>
139
+ <div class="canvas-wrap"><canvas id="lossGraph"></canvas></div>
140
+ <div class="hint">Loss: white = train, gray = val.</div>
141
+ <div class="bar"><i id="epochBar"></i></div>
142
+ <div id="stats" class="block stat"></div>
143
+
144
+ <h4 class="title">Visualization</h4>
145
+ <div class="canvas-wrap"><canvas id="networkGraph"></canvas></div>
146
+ <div class="hint">Internal representation.</div>
147
+ </section>
148
+
149
+ <!-- RIGHT: Control / Predict -->
150
+ <section class="panel" aria-label="Control">
151
+ <h4 class="title">Control</h4>
152
+ <div class="block btns">
153
+ <button id="trainButton">Train</button>
154
+ <button id="saveButton">Save</button>
155
+ <button id="loadButton">Load</button>
156
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
+ <h4 class="title">Predict</h4>
159
+ <div class="block pred">
160
+ <input class="input" type="text" id="predictionInput" placeholder="0.4, 0.2, 0.6" />
161
+ <button id="predictButton">Predict</button>
162
+ </div>
163
+ <div id="predictionResult" class="block"></div>
 
 
 
 
 
 
 
 
 
 
164
 
165
+ <div class="block">
166
+ <div class="hint">Repo: <a href="https://github.com/appvoid/carbono" target="_blank" rel="noopener">github/appvoid/carbono</a></div>
167
+ </div>
168
+ </section>
 
169
  </div>
170
  </div>
171
 
172
  <script>
173
+ /* --------- Fit-to-viewport scaling (no scroll) --------- */
174
+ (function(){
175
+ const baseW = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--base-w'),10);
176
+ const baseH = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--base-h'),10);
177
+ const fitEl = document.getElementById('fit');
178
+ function fit(){ const s = Math.min(window.innerWidth/baseW, window.innerHeight/baseH); fitEl.style.transform = `scale(${s})`; }
179
+ window.addEventListener('resize', fit, {passive:true}); fit();
180
+ })();
181
+
182
+ /* ---------------- Carbono micro-lib (unchanged API; minor fixes) ---------------- */
183
+ class carbono {
184
+ constructor(debug = true) { this.layers=[]; this.weights=[]; this.biases=[]; this.activations=[]; this.details={}; this.debug=debug; }
185
+ layer(inputSize, outputSize, activation='tanh'){
186
+ this.layers.push({inputSize,outputSize,activation});
187
+ if(this.weights.length>0){
188
+ const lastOut = this.layers[this.layers.length-2].outputSize;
189
+ if(inputSize!==lastOut) throw new Error('Input size must match previous layer output size.');
190
+ }
191
+ const W=[]; for(let i=0;i<outputSize;i++){ const row=[]; for(let j=0;j<inputSize;j++){ row.push((Math.random()-0.5)*2*Math.sqrt(6/(inputSize+outputSize))); } W.push(row); }
192
+ this.weights.push(W); this.biases.push(Array(outputSize).fill(0.01)); this.activations.push(activation);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  }
194
+ activationFunction(x,a){ switch(a){case 'tanh':return Math.tanh(x);case 'sigmoid':return 1/(1+Math.exp(-x));case 'relu':return Math.max(0,x);case 'selu':{const alpha=1.67326,scale=1.0507;return x>0?scale*x:scale*alpha*(Math.exp(x)-1);}default:throw new Error('Unknown activation');} }
195
+ activationDerivative(x,a){ switch(a){case 'tanh':return 1-Math.pow(Math.tanh(x),2);case 'sigmoid':{const s=1/(1+Math.exp(-x));return s*(1-s);}case 'relu':return x>0?1:0;case 'selu':{const alpha=1.67326,scale=1.0507;return x>0?scale:scale*alpha*Math.exp(x);}default:throw new Error('Unknown derivative');} }
196
+ positionalEncoding(input,maxLen){ const pe=new Array(maxLen).fill(0).map((_,pos)=>new Array(input[0].length).fill(0).map((_,i)=>{const ang=pos/Math.pow(10000,2*i/input[0].length);return pos%2===0?Math.sin(ang):Math.cos(ang);})); return input.map((seq,idx)=>seq.map((v,i)=>v+pe[idx][i])); }
197
+ multiHeadSelfAttention(input,numHeads=2){
198
+ const headSize=input[0].length/numHeads; const heads=[...Array(numHeads)].map(()=>[...Array(input.length)].map(()=>[...Array(headSize)].fill(0)));
199
+ for(let h=0;h<numHeads;h++) for(let i=0;i<input.length;i++) for(let j=0;j<headSize;j++) heads[h][i][j]=input[i][h*headSize+j];
200
+ const scores=[...Array(numHeads)].map(()=>[...Array(input.length)].map(()=>[...Array(input.length)].fill(0)));
201
+ for(let h=0;h<numHeads;h++) for(let i=0;i<input.length;i++) for(let j=0;j<input.length;j++){ let s=0; for(let k=0;k<headSize;k++) s+=heads[h][i][k]*heads[h][j][k]; scores[h][i][j]=s; }
202
+ const weights=scores.map(head=>head.map(row=>{const ex=row.map(v=>Math.exp(v)); const sum=ex.reduce((a,b)=>a+b,0); return ex.map(v=>v/sum)}));
203
+ const out=[...Array(input.length)].map(()=>[...Array(input[0].length)].fill(0));
204
+ for(let h=0;h<numHeads;h++) for(let i=0;i<input.length;i++) for(let j=0;j<headSize;j++) for(let k=0;k<input.length;k++) out[i][h*headSize+j]+=weights[h][i][k]*heads[h][k][j];
205
+ return out;
206
  }
207
+ layerNormalization(arr){ const m=arr.reduce((s,v)=>s+v,0)/arr.length; const v=arr.reduce((s,x)=>s+Math.pow(x-m,2),0)/arr.length; return arr.map(x=>(x-m)/Math.sqrt(v+1e-5)); }
208
+ async train(trainSet,options={}){
209
+ const {epochs=200,learningRate=0.212,batchSize=16,printEveryEpochs=100,earlyStopThreshold=1e-6,testSet=null,callback=null}=options;
210
+ const start=Date.now(); const batch=Math.max(1,batchSize);
211
+ if(this.layers.length===0){ const n=trainSet[0].input.length; this.layer(n,n,'tanh'); this.layer(n,1,'tanh'); }
212
+ let lastTrainLoss=0,lastTestLoss=null;
213
+ for(let epoch=0;epoch<epochs;epoch++){
214
+ let trainError=0;
215
+ for(let b=0;b<trainSet.length;b+=batch){
216
+ const batchItems=trainSet.slice(b,b+batch); let batchError=0;
217
+ for(const data of batchItems){
218
+ const L=[data.input];
219
+ for(let i=0;i<this.weights.length;i++){
220
+ const inputs=L[i], W=this.weights[i], B=this.biases[i], act=this.activations[i]; const out=[];
221
+ for(let j=0;j<W.length;j++){ const w=W[j]; let sum=B[j]; for(let k=0;k<inputs.length;k++) sum+=inputs[k]*w[k]; out.push(this.activationFunction(sum,act)); }
222
+ L.push(out);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  }
224
+ const outIn=L[L.length-1]; const outErr=[]; for(let i=0;i<outIn.length;i++) outErr.push((data.output[i]??0)-outIn[i]);
225
+ let layerErrors=[outErr];
226
+ for(let i=this.weights.length-2;i>=0;i--){
227
+ const Wnext=this.weights[i+1], nextErr=layerErrors[0], curIn=L[i+1], act=this.activations[i]; const errs=[];
228
+ for(let j=0;j<this.layers[i].outputSize;j++){ let e=0; for(let k=0;k<this.layers[i+1].outputSize;k++) e+=nextErr[k]*Wnext[k][j]; errs.push(e*this.activationDerivative(curIn[j],act)); }
229
+ layerErrors.unshift(errs);
 
 
 
 
 
 
 
 
 
230
  }
231
+ for(let i=0;i<this.weights.length;i++){
232
+ const inputs=L[i], errs=layerErrors[i], W=this.weights[i], B=this.biases[i];
233
+ for(let j=0;j<W.length;j++){ const w=W[j]; for(let k=0;k<inputs.length;k++) w[k]+=learningRate*errs[j]*inputs[k]; B[j]+=learningRate*errs[j]; }
 
 
 
 
 
 
 
 
 
234
  }
235
+ batchError+=Math.abs(outErr[0]??0);
236
  }
237
+ trainError+=batchError;
238
  }
239
+ lastTrainLoss=trainError/trainSet.length;
240
+ if(testSet){ let te=0; for(const d of testSet){ const p=this.predict(d.input); te+=Math.abs((d.output[0]??0)-(p[0]??0)); } lastTestLoss=te/testSet.length; }
241
+ if((epoch+1)%printEveryEpochs===0 && this.debug) console.log(`Epoch ${epoch+1} | Train ${lastTrainLoss.toFixed(6)}${testSet?` | Val ${lastTestLoss.toFixed(6)}`:''}`);
242
+ if(callback) await callback(epoch+1,lastTrainLoss,lastTestLoss);
243
+ await new Promise(r=>setTimeout(r,0));
244
+ if(lastTrainLoss<earlyStopThreshold) { if(this.debug) console.log(`Early stop @${epoch+1}`); break; }
245
+ }
246
+ const end=Date.now(); let params=0; for(let i=0;i<this.weights.length;i++){ params+=this.weights[i].flat().length+this.biases[i].length; }
247
+ const summary={trainLoss:lastTrainLoss,testLoss:lastTestLoss,parameters:params,training:{time:end-start,epochs,learningRate,batchSize:batch},layers:this.layers.map(l=>({inputSize:l.inputSize,outputSize:l.outputSize,activation:l.activation}))};
248
+ this.details=summary; return summary;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  }
250
+ predict(input){
251
+ let x=input; const acts=[input], raw=[];
252
+ for(let i=0;i<this.weights.length;i++){ const W=this.weights[i], B=this.biases[i], a=this.activations[i]; const y=[], r=[];
253
+ for(let j=0;j<W.length;j++){ const w=W[j]; let s=B[j]; for(let k=0;k<x.length;k++) s+=x[k]*w[k]; r.push(s); y.push(this.activationFunction(s,a)); }
254
+ raw.push(r); acts.push(y); x=y;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  }
256
+ this.lastActivations=acts; this.lastRawValues=raw; return x;
 
 
257
  }
258
+ save(name='model'){ const data={weights:this.weights,biases:this.biases,activations:this.activations,layers:this.layers,details:this.details};
259
+ const blob=new Blob([JSON.stringify(data)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=`${name}.json`; a.click(); URL.revokeObjectURL(url);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  }
261
+ load(callback){
262
+ const onChange=(e)=>{ const f=e.target.files[0]; if(!f) return; const r=new FileReader();
263
+ r.onload=(ev)=>{ try{ const data=JSON.parse(ev.target.result); this.weights=data.weights; this.biases=data.biases; this.activations=data.activations; this.layers=data.layers; this.details=data.details; callback&&callback(); if(this.debug) console.log('Loaded'); }catch(err){ if(this.debug) console.error('Load failed',err); } finally{ input.removeEventListener('change',onChange); input.remove(); } };
264
+ r.readAsText(f);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  };
266
+ const input=document.createElement('input'); input.type='file'; input.accept='.json'; input.style.position='fixed'; input.style.opacity='0'; document.body.append(input); input.addEventListener('change',onChange); input.click();
 
 
 
 
 
 
267
  }
268
  }
269
 
270
+ /* ---------------- App ---------------- */
271
+ document.addEventListener('DOMContentLoaded',()=>{
272
+ const nn=new carbono();
273
+ let lossHistory=[];
274
+
275
+ const lossCanvas=document.getElementById('lossGraph');
276
+ const networkCanvas=document.getElementById('networkGraph');
277
+ const lossCtx=lossCanvas.getContext('2d');
278
+
279
+ const el={
280
+ loadDataBtn:document.getElementById('loadDataBtn'),
281
+ trainingData:document.getElementById('trainingData'),
282
+ testData:document.getElementById('testData'),
283
+ numHiddenLayers:document.getElementById('numHiddenLayers'),
284
+ hiddenLayersConfig:document.getElementById('hiddenLayersConfig'),
285
+ trainButton:document.getElementById('trainButton'),
286
+ stats:document.getElementById('stats'),
287
+ epochBar:document.getElementById('epochBar'),
288
+ epochs:document.getElementById('epochs'),
289
+ learningRate:document.getElementById('learningRate'),
290
+ batchSize:document.getElementById('batchSize'),
291
+ predictButton:document.getElementById('predictButton'),
292
+ predictionInput:document.getElementById('predictionInput'),
293
+ predictionResult:document.getElementById('predictionResult'),
294
+ saveButton:document.getElementById('saveButton'),
295
+ loadButton:document.getElementById('loadButton')
296
+ };
297
+
298
+ const parseCSV=(csv)=> csv.trim().split('\n').filter(Boolean).map(row=>{
299
+ const values=row.split(',').map(s=>Number(s.trim()));
300
+ return {input:values.slice(0,-1),output:[values[values.length-1]]};
301
+ });
302
+
303
+ function drawLossGraph(){
304
+ const {width,height}=lossCanvas;
305
+ lossCtx.clearRect(0,0,width,height);
306
+ if(lossHistory.length===0) return;
307
+ const maxLoss=Math.max(1e-9,...lossHistory.map(l=>Math.max(l.train, l.test??0)));
308
+ function line(data, color){
309
+ lossCtx.strokeStyle=color; lossCtx.beginPath();
310
+ data.forEach((v,i)=>{ const x=(i/(data.length-1))*width; const y=height-(v/maxLoss)*height; if(i===0) lossCtx.moveTo(x,y); else lossCtx.lineTo(x,y); });
311
+ lossCtx.stroke();
312
+ }
313
+ line(lossHistory.map(l=>l.train),'#ffffff');
314
+ if(lossHistory.some(l=>l.test!==undefined)) line(lossHistory.map(l=>l.test ?? 0),'#777777');
315
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
+ function createLayerConfigUI(n){
318
+ el.hiddenLayersConfig.innerHTML='';
319
+ for(let i=0;i<n;i++){
320
+ const block=document.createElement('div');
321
+ block.className='row';
322
+ block.style.marginTop='0';
323
+ block.innerHTML=`
324
+ <div style="grid-column: span 2;" class="block">
325
+ <label>Layer ${i+1} Nodes</label>
326
+ <input class="input" type="number" value="5" data-layer-index="${i}">
327
+ </div>
328
+ <div style="grid-column: span 2;" class="block">
329
+ <label>Activation</label>
330
+ <select class="input" data-layer-index="${i}">
331
+ <option>tanh</option>
332
+ <option>sigmoid</option>
333
+ <option>relu</option>
334
+ <option>selu</option>
335
+ </select>
336
+ </div>`;
337
+ el.hiddenLayersConfig.appendChild(block);
338
+ }
339
+ }
340
 
341
+ async function trainModel(){
342
+ lossHistory=[];
343
+ const trainingData=parseCSV(el.trainingData.value);
344
+ const testData=parseCSV(el.testData.value||'');
345
+ el.stats.innerHTML='';
346
+
347
+ const nHidden=parseInt(el.numHiddenLayers.value,10);
348
+ const layerCfg=[];
349
+ for(let i=0;i<nHidden;i++){
350
+ const size=parseInt(document.querySelector(`input[data-layer-index="${i}"]`).value,10);
351
+ const act=document.querySelector(`select[data-layer-index="${i}"]`).value;
352
+ layerCfg.push({size,activation:act});
353
+ }
354
+
355
+ nn.layers=[]; nn.weights=[]; nn.biases=[]; nn.activations=[];
356
+ const numInputs=trainingData[0].input.length;
357
+ nn.layer(numInputs, layerCfg[0].size, layerCfg[0].activation);
358
+ for(let i=1;i<layerCfg.length;i++) nn.layer(layerCfg[i-1].size, layerCfg[i].size, layerCfg[i].activation);
359
+ nn.layer(layerCfg[layerCfg.length-1].size, 1, 'tanh');
360
+
361
+ const opts={
362
+ epochs:parseInt(el.epochs.value,10),
363
+ learningRate:parseFloat(el.learningRate.value),
364
+ batchSize:parseInt(el.batchSize.value,10),
365
+ printEveryEpochs:1,
366
+ testSet:testData.length?testData:null,
367
+ callback:async (epoch,trainLoss,testLoss)=>{
368
+ lossHistory.push({train:trainLoss,test:testLoss});
369
+ drawLossGraph();
370
+ el.epochBar.style.width=`${(epoch/opts.epochs)*100}%`;
371
+ el.stats.innerHTML=`
372
+ <div><b>Epoch</b></div><div>${epoch}/${opts.epochs}</div>
373
+ <div><b>Train</b></div><div>${trainLoss.toFixed(6)}</div>
374
+ ${testLoss!==null?`<div><b>Val</b></div><div>${testLoss.toFixed(6)}</div>`:''}
375
+ `;
376
  }
377
+ };
378
 
379
+ try{ el.trainButton.disabled=true; el.trainButton.textContent='Training…'; await nn.train(trainingData,opts);
380
+ el.stats.innerHTML+=`<div><b>Status</b></div><div>Model trained</div>`;
381
+ }catch(e){ console.error('Training error:',e);
382
+ el.stats.innerHTML+=`<div><b>Error</b></div><div>${e.message}</div>`;
383
+ }finally{ el.trainButton.disabled=false; el.trainButton.textContent='Train'; }
384
+ }
 
 
385
 
386
+ function drawNetwork(){
387
+ const ctx=networkCanvas.getContext('2d');
388
+ ctx.clearRect(0,0,networkCanvas.width,networkCanvas.height);
389
+ if(!nn.lastActivations) return;
 
 
 
 
 
390
 
391
+ const pad=34; const W=networkCanvas.width-pad*2; const H=networkCanvas.height-pad*2;
392
+ const layers=[];
 
 
 
 
 
 
 
 
393
 
394
+ // inputs
395
+ const inSize=nn.layers[0].inputSize; const inX=pad; const inNodes=[];
396
+ for(let i=0;i<inSize;i++){ const y=pad+(inSize>1?(H*i)/(inSize-1):H/2); inNodes.push({x:inX,y,val:nn.lastActivations[0][i]||0}); }
397
+ layers.push(inNodes);
 
398
 
399
+ // hidden(s)
400
+ for(let i=1;i<nn.lastActivations.length-1;i++){
401
+ const L=nn.lastActivations[i]; const nodes=[]; const x=pad+(W*i)/(nn.lastActivations.length-1);
402
+ for(let j=0;j<L.length;j++){ const y=pad+(L.length>1?(H*j)/(L.length-1):H/2); nodes.push({x,y,val:L[j]}); }
403
+ layers.push(nodes);
404
+ }
 
 
 
 
 
 
 
 
 
 
 
 
405
 
406
+ // output
407
+ const outX=networkCanvas.width-pad; const outY=pad+H/2; layers.push([{x:outX,y:outY,val:nn.lastActivations.at(-1)[0]||0}]);
 
 
 
 
 
 
 
 
 
 
 
 
408
 
409
+ // connections
410
+ ctx.lineWidth=1;
411
+ for(let i=0;i<layers.length-1;i++){
412
+ const A=layers[i], B=layers[i+1], Wmat=nn.weights[i];
413
+ for(let j=0;j<A.length;j++) for(let k=0;k<B.length;k++){
414
+ const w=Wmat[k][j]; const sig=Math.abs((A[j].val||0)*w); const op=Math.min(Math.max(sig,0.06),1);
415
+ ctx.strokeStyle=`rgba(255,255,255,${op})`; ctx.beginPath(); ctx.moveTo(A[j].x,A[j].y); ctx.lineTo(B[k].x,B[k].y); ctx.stroke();
416
  }
417
+ }
418
+ // nodes
419
+ for(const L of layers){ for(const n of L){ const r=3.5, op=Math.min(Math.max(Math.abs(n.val),0.3),1);
420
+ ctx.fillStyle=`rgba(255,255,255,${op})`; ctx.beginPath(); ctx.arc(n.x,n.y,r,0,Math.PI*2); ctx.fill();
421
+ ctx.strokeStyle='rgba(255,255,255,1)'; ctx.lineWidth=.8; ctx.stroke();
422
+ }}
423
+ }
424
 
425
+ function sizeCanvases(){
426
+ [lossCanvas,networkCanvas].forEach(cv=>{ cv.width=cv.parentElement.clientWidth; cv.height=cv.parentElement.clientHeight; });
427
+ drawNetwork();
428
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
 
430
+ el.loadDataBtn.onclick=()=>{ el.trainingData.value=`1.0, 0.0, 0.0, 0.0
431
+ 0.7, 0.7, 0.8, 1
432
+ 0.0, 1.0, 0.0, 0.5`; el.testData.value=`0.4, 0.2, 0.6, 1.0
433
+ 0.2, 0.82, 0.83, 1.0`; };
434
+
435
+ el.numHiddenLayers.addEventListener('change',(e)=>createLayerConfigUI(parseInt(e.target.value,10)));
436
+ el.trainButton.addEventListener('click',trainModel);
437
+ el.predictButton.addEventListener('click',()=>{
438
+ const input=el.predictionInput.value.split(',').map(s=>Number(s.trim())).filter(n=>!Number.isNaN(n));
439
+ const p=nn.predict(input);
440
+ el.predictionResult.textContent=`Prediction: ${Number.isFinite(p[0])?p[0].toFixed(6):'NaN'}`;
441
+ drawNetwork();
442
+ });
443
+ el.saveButton.addEventListener('click',()=>nn.save('model'));
444
+ el.loadButton.addEventListener('click',()=>nn.load(()=>{ el.stats.innerHTML+=`<div><b>Status</b></div><div>Model loaded</div>`; }));
445
+
446
+ window.addEventListener('resize', sizeCanvases, {passive:true});
447
+
448
+ createLayerConfigUI(parseInt(el.numHiddenLayers.value,10));
449
+ sizeCanvases();
450
+ });
451
  </script>
452
  </body>
 
453
  </html>