|
<!DOCTYPE html> |
|
<html> |
|
|
|
<head> |
|
<title>Carbono UI</title> |
|
<style> |
|
a { |
|
color: white; |
|
} |
|
|
|
body { |
|
background: #000; |
|
color: #fff; |
|
font-family: monospace; |
|
margin: 0; |
|
padding-top: 16px; |
|
padding: 5%; |
|
display: flex; |
|
flex-direction: column; |
|
gap: 15px; |
|
overflow-x: hidden; |
|
} |
|
|
|
h3 { |
|
margin: 1.5rem; |
|
margin-bottom: 0; |
|
} |
|
|
|
p { |
|
margin: 1.5rem; |
|
margin-top: 0rem; |
|
color: #777; |
|
} |
|
|
|
.grid { |
|
display: grid; |
|
grid-template-columns: minmax(400px, 1fr) minmax(300px, 2fr); |
|
gap: 15px; |
|
opacity: 0; |
|
transform: translateY(20px); |
|
animation: fadeInUp 0.5s ease-out forwards; |
|
} |
|
|
|
.widget { |
|
background: #000; |
|
border-radius: 10px; |
|
padding: 15px; |
|
box-sizing: border-box; |
|
width: 100%; |
|
opacity: 0; |
|
transform: translateY(20px); |
|
animation: fadeInUp 0.5s ease-out forwards; |
|
animation-delay: 0.2s; |
|
} |
|
|
|
.widget-title { |
|
font-size: 1.1em; |
|
margin-bottom: 12px; |
|
border-bottom: 1px solid #333; |
|
padding-bottom: 8px; |
|
opacity: 0; |
|
transform: translateY(10px); |
|
animation: fadeInUp 0.5s ease-out forwards; |
|
animation-delay: 0.3s; |
|
} |
|
|
|
.input-group { |
|
margin-bottom: 12px; |
|
opacity: 0; |
|
transform: translateY(10px); |
|
animation: fadeInUp 0.5s ease-out forwards; |
|
animation-delay: 0.4s; |
|
} |
|
|
|
.settings-grid { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); |
|
gap: 10px; |
|
margin-bottom: 12px; |
|
opacity: 0; |
|
transform: translateY(10px); |
|
animation: fadeInUp 0.5s ease-out forwards; |
|
animation-delay: 0.5s; |
|
} |
|
|
|
input[type="text"], |
|
input[type="number"], |
|
select, |
|
textarea { |
|
outline: none; |
|
width: 100%; |
|
padding: 6px; |
|
background: #222; |
|
border: 1px solid #444; |
|
color: #fff; |
|
border-radius: 8px; |
|
margin-top: 4px; |
|
box-sizing: border-box; |
|
transition: background 0.3s, border 0.3s; |
|
} |
|
|
|
span { |
|
background-color: white; |
|
color: black; |
|
font-weight: 600; |
|
font-size: 12px; |
|
padding: 1px; |
|
border-radius: 3px; |
|
cursor: pointer; |
|
} |
|
|
|
input[type="text"]:focus, |
|
input[type="number"]:focus, |
|
select:focus, |
|
textarea:focus { |
|
background: #333; |
|
border: 1px solid #666; |
|
} |
|
|
|
button { |
|
background: #fff; |
|
color: #000; |
|
border: none; |
|
padding: 6px 12px; |
|
border-radius: 6px; |
|
cursor: pointer; |
|
transition: all 0.1s ease; |
|
border: 1px solid white; |
|
opacity: 0; |
|
height: 28px; |
|
transform: translateY(10px); |
|
animation: fadeInUp 0.5s ease-out forwards; |
|
animation-delay: 0.6s; |
|
} |
|
|
|
button:hover { |
|
border: 1px solid white; |
|
color: white; |
|
background: #000; |
|
} |
|
|
|
.progress-container { |
|
height: 180px; |
|
position: relative; |
|
border: 1px solid #333; |
|
border-radius: 8px; |
|
margin-bottom: 10px; |
|
opacity: 0; |
|
transform: translateY(10px); |
|
animation: fadeInUp 0.5s ease-out forwards; |
|
animation-delay: 0.7s; |
|
} |
|
|
|
.loss-graph { |
|
position: absolute; |
|
bottom: 0; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.network-graph { |
|
position: absolute; |
|
bottom: 0; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.flex-container { |
|
display: flex; |
|
gap: 20px; |
|
opacity: 0; |
|
transform: translateY(10px); |
|
animation: fadeInUp 0.5s ease-out forwards; |
|
animation-delay: 0.8s; |
|
} |
|
|
|
.prediction-section, |
|
.model-section { |
|
flex: 1; |
|
} |
|
|
|
.button-group { |
|
display: flex; |
|
gap: 10px; |
|
opacity: 0; |
|
transform: translateY(10px); |
|
animation: fadeInUp 0.5s ease-out forwards; |
|
animation-delay: 0.9s; |
|
} |
|
|
|
.visualization-container { |
|
margin-top: 15px; |
|
opacity: 0; |
|
transform: translateY(10px); |
|
animation: fadeInUp 0.5s ease-out forwards; |
|
animation-delay: 1s; |
|
} |
|
|
|
.epoch-progress { |
|
height: 5px; |
|
background: #222; |
|
border-radius: 8px; |
|
overflow: hidden; |
|
} |
|
|
|
.epoch-bar { |
|
height: 100%; |
|
width: 0; |
|
background: #fff; |
|
transition: width 0.3s ease; |
|
} |
|
|
|
@keyframes fadeInUp { |
|
to { |
|
opacity: 1; |
|
transform: translateY(0); |
|
} |
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
.grid { |
|
grid-template-columns: 1fr; |
|
} |
|
|
|
.flex-container { |
|
flex-direction: column; |
|
} |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<h3>playground</h3> |
|
<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; it's the easiest, hackable machine learning js library; it's also convenient to quickly prototype on embedded devices. 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 "train" button.</p> |
|
<div class="grid"> |
|
|
|
<div class="widget"> |
|
<div class="widget-title">model settings</div> |
|
|
|
<div class="input-group"> |
|
<label>training set:</label> |
|
<textarea id="trainingData" rows="3" placeholder="1,1,1,0 |
|
1,0,1,0 |
|
0,1,0,1"></textarea> |
|
</div> |
|
<p>last number represents actual desired output</p> |
|
<div class="input-group"> |
|
<label>validation set:</label> |
|
<textarea id="testData" rows="3" placeholder="0,0,0,1"></textarea> |
|
</div> |
|
|
|
<div class="settings-grid"> |
|
<div class="input-group"> |
|
<label>epochs:</label> |
|
<input type="number" id="epochs" value="50"> |
|
</div> |
|
<div class="input-group"> |
|
<label>learning rate:</label> |
|
<input type="number" id="learningRate" value="0.1" step="0.001"> |
|
</div> |
|
<div class="input-group"> |
|
<label>batch size:</label> |
|
<input type="number" id="batchSize" value="8"> |
|
</div> |
|
<div class="input-group"> |
|
<label>hidden layers:</label> |
|
<input type="number" id="numHiddenLayers" value="1"> |
|
</div> |
|
</div> |
|
|
|
|
|
|
|
<div id="hiddenLayersConfig"></div> |
|
</div> |
|
|
|
|
|
<div class="widget"> |
|
<div class="widget-title">training progress</div> |
|
<div id="progress"> |
|
<div class="progress-container"> |
|
<canvas id="lossGraph" class="loss-graph"></canvas> |
|
</div> |
|
<p>training loss is white, validation loss is gray</p> |
|
<div class="epoch-progress"> |
|
<div id="epochBar" class="epoch-bar"></div> |
|
</div> |
|
<div id="stats" style="margin-top: 10px;"></div> |
|
</div> |
|
<div class="model-section"> |
|
<br> |
|
<div class="widget-title">model management</div> |
|
<p>save the weights to load them on your app or share them on huggingface!</p> |
|
<div class="button-group"> |
|
<button id="trainButton">train</button> |
|
<button id="saveButton">save</button> |
|
<button id="loadButton">load</button> |
|
<div class="prediction-section"> |
|
<div class="widget-title">prediction</div> |
|
<p>predict output</p> |
|
<div class="input-group"> |
|
<label>input:</label> |
|
<input type="text" id="predictionInput" placeholder="0.4, 0.2, 0.6"> |
|
</div> |
|
<button id="predictButton">predict</button> |
|
<div id="predictionResult" style="margin-top: 10px;"></div> |
|
</div> |
|
<div class="visualization-container"> |
|
<div class="widget-title">visualization</div> |
|
<div class="progress-container"> |
|
<canvas id="networkGraph" class="network-graph"></canvas> |
|
</div> |
|
<p>internal model's representation</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
class carbono { |
|
constructor(debug = true) { |
|
this.layers = []; |
|
this.weights = []; |
|
this.biases = []; |
|
this.activations = []; |
|
this.details = {}; |
|
this.debug = debug; |
|
} |
|
|
|
|
|
layer(inputSize, outputSize, activation = 'tanh') { |
|
this.layers.push({ |
|
inputSize, |
|
outputSize, |
|
activation |
|
}); |
|
if (this.weights.length > 0) { |
|
const lastLayerOutputSize = this.layers[this.layers.length - 2].outputSize; |
|
if (inputSize !== lastLayerOutputSize) { |
|
throw new Error('Oops! The input size of the new layer must match the output size of the previous layer.'); |
|
} |
|
} |
|
const weights = []; |
|
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))); |
|
} |
|
weights.push(row); |
|
} |
|
this.weights.push(weights); |
|
const biases = Array(outputSize).fill(0.01); |
|
this.biases.push(biases); |
|
this.activations.push(activation); |
|
} |
|
|
|
|
|
activationFunction(x, activation) { |
|
switch (activation) { |
|
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; |
|
const scale = 1.0507; |
|
return x > 0 ? scale * x : scale * alpha * (Math.exp(x) - 1); |
|
default: |
|
throw new Error('Whoops! We don\'t know that activation function.'); |
|
} |
|
} |
|
|
|
|
|
activationDerivative(x, activation) { |
|
switch (activation) { |
|
case 'tanh': |
|
return 1 - Math.pow(Math.tanh(x), 2); |
|
case 'sigmoid': |
|
const sigmoid = 1 / (1 + Math.exp(-x)); |
|
return sigmoid * (1 - sigmoid); |
|
case 'relu': |
|
return x > 0 ? 1 : 0; |
|
case 'selu': |
|
const alpha = 1.67326; |
|
const scale = 1.0507; |
|
return x > 0 ? scale : scale * alpha * Math.exp(x); |
|
default: |
|
throw new Error('Oops! We don\'t know the derivative of that activation function.'); |
|
} |
|
} |
|
|
|
|
|
positionalEncoding(input, maxLen) { |
|
const pe = new Array(maxLen).fill(0).map((_, pos) => { |
|
return new Array(input[0].length).fill(0).map((_, i) => { |
|
const angle = pos / Math.pow(10000, 2 * i / input[0].length); |
|
return pos % 2 === 0 ? Math.sin(angle) : Math.cos(angle); |
|
}); |
|
}); |
|
return input.map((seq, idx) => seq.map((val, i) => val + pe[idx][i])); |
|
} |
|
|
|
|
|
multiHeadSelfAttention(input, numHeads = 2) { |
|
const headSize = input[0].length / numHeads; |
|
const heads = new Array(numHeads).fill(0).map(() => new Array(input.length).fill(0).map(() => new Array(headSize).fill(0))); |
|
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]; |
|
} |
|
} |
|
} |
|
const attentionScores = new Array(numHeads).fill(0).map(() => new Array(input.length).fill(0).map(() => new Array(input.length).fill(0))); |
|
for (let h = 0; h < numHeads; h++) { |
|
for (let i = 0; i < input.length; i++) { |
|
for (let j = 0; j < input.length; j++) { |
|
let score = 0; |
|
for (let k = 0; k < headSize; k++) { |
|
score += heads[h][i][k] * heads[h][j][k]; |
|
} |
|
attentionScores[h][i][j] = score; |
|
} |
|
} |
|
} |
|
const attentionWeights = attentionScores.map(head => head.map(row => row.map(score => Math.exp(score) / row.reduce((sum, s) => sum + Math.exp(s), 0)))); |
|
const output = new Array(input.length).fill(0).map(() => new Array(input[0].length).fill(0)); |
|
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++) { |
|
output[i][h * headSize + j] += attentionWeights[h][i][k] * heads[h][k][j]; |
|
} |
|
} |
|
} |
|
} |
|
return output; |
|
} |
|
|
|
|
|
layerNormalization(input) { |
|
const mean = input.reduce((sum, val) => sum + val, 0) / input.length; |
|
const variance = input.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / input.length; |
|
return input.map(val => (val - mean) / Math.sqrt(variance + 1e-5)); |
|
} |
|
|
|
|
|
async train(trainSet, options = {}) { |
|
const { |
|
epochs = 200, |
|
learningRate = 0.212, |
|
batchSize = 16, |
|
printEveryEpochs = 100, |
|
earlyStopThreshold = 1e-6, |
|
testSet = null, |
|
callback = null |
|
} = options; |
|
const start = Date.now(); |
|
if (batchSize < 1) batchSize = 2; |
|
if (this.layers.length === 0) { |
|
const numInputs = trainSet[0].input.length; |
|
this.layer(numInputs, numInputs, 'tanh'); |
|
this.layer(numInputs, 1, 'tanh'); |
|
} |
|
let lastTrainLoss = 0; |
|
let lastTestLoss = null; |
|
|
|
for (let epoch = 0; epoch < epochs; epoch++) { |
|
let trainError = 0; |
|
for (let b = 0; b < trainSet.length; b += batchSize) { |
|
const batch = trainSet.slice(b, b + batchSize); |
|
let batchError = 0; |
|
for (const data of batch) { |
|
const layerInputs = [data.input]; |
|
for (let i = 0; i < this.weights.length; i++) { |
|
const inputs = layerInputs[i]; |
|
const weights = this.weights[i]; |
|
const biases = this.biases[i]; |
|
const activation = this.activations[i]; |
|
const outputs = []; |
|
for (let j = 0; j < weights.length; j++) { |
|
const weight = weights[j]; |
|
let sum = biases[j]; |
|
for (let k = 0; k < inputs.length; k++) { |
|
sum += inputs[k] * weight[k]; |
|
} |
|
outputs.push(this.activationFunction(sum, activation)); |
|
} |
|
layerInputs.push(outputs); |
|
} |
|
const outputLayerIndex = this.weights.length - 1; |
|
const outputLayerInputs = layerInputs[layerInputs.length - 1]; |
|
const outputErrors = []; |
|
for (let i = 0; i < outputLayerInputs.length; i++) { |
|
const error = data.output[i] - outputLayerInputs[i]; |
|
outputErrors.push(error); |
|
} |
|
let layerErrors = [outputErrors]; |
|
for (let i = this.weights.length - 2; i >= 0; i--) { |
|
const nextLayerWeights = this.weights[i + 1]; |
|
const nextLayerErrors = layerErrors[0]; |
|
const currentLayerInputs = layerInputs[i + 1]; |
|
const currentActivation = this.activations[i]; |
|
const errors = []; |
|
for (let j = 0; j < this.layers[i].outputSize; j++) { |
|
let error = 0; |
|
for (let k = 0; k < this.layers[i + 1].outputSize; k++) { |
|
error += nextLayerErrors[k] * nextLayerWeights[k][j]; |
|
} |
|
errors.push(error * this.activationDerivative(currentLayerInputs[j], currentActivation)); |
|
} |
|
layerErrors.unshift(errors); |
|
} |
|
for (let i = 0; i < this.weights.length; i++) { |
|
const inputs = layerInputs[i]; |
|
const errors = layerErrors[i]; |
|
const weights = this.weights[i]; |
|
const biases = this.biases[i]; |
|
for (let j = 0; j < weights.length; j++) { |
|
const weight = weights[j]; |
|
for (let k = 0; k < inputs.length; k++) { |
|
weight[k] += learningRate * errors[j] * inputs[k]; |
|
} |
|
biases[j] += learningRate * errors[j]; |
|
} |
|
} |
|
batchError += Math.abs(outputErrors[0]); |
|
} |
|
trainError += batchError; |
|
} |
|
lastTrainLoss = trainError / trainSet.length; |
|
if (testSet) { |
|
let testError = 0; |
|
for (const data of testSet) { |
|
const prediction = this.predict(data.input); |
|
testError += Math.abs(data.output[0] - prediction[0]); |
|
} |
|
lastTestLoss = testError / testSet.length; |
|
} |
|
|
|
if ((epoch + 1) % printEveryEpochs === 0 && this.debug === true) { |
|
console.log(`Epoch ${epoch + 1}, Train Loss: ${lastTrainLoss.toFixed(6)}${testSet ? `, Test Loss: ${lastTestLoss.toFixed(6)}` : ''}`); |
|
} |
|
if (callback) { |
|
await callback(epoch + 1, lastTrainLoss, lastTestLoss); |
|
} |
|
await new Promise(resolve => setTimeout(resolve, 0)); |
|
if (lastTrainLoss < earlyStopThreshold) { |
|
console.log(`We stopped at epoch ${epoch + 1} with train loss: ${lastTrainLoss.toFixed(6)}${testSet ? ` and test loss: ${lastTestLoss.toFixed(6)}` : ''}`); |
|
break; |
|
} |
|
} |
|
const end = Date.now(); |
|
let totalParams = 0; |
|
for (let i = 0; i < this.weights.length; i++) { |
|
const weightLayer = this.weights[i]; |
|
const biasLayer = this.biases[i]; |
|
totalParams += weightLayer.flat().length + biasLayer.length; |
|
} |
|
const trainingSummary = { |
|
trainLoss: lastTrainLoss, |
|
testLoss: lastTestLoss, |
|
parameters: totalParams, |
|
training: { |
|
time: end - start, |
|
epochs, |
|
learningRate, |
|
batchSize |
|
}, |
|
layers: this.layers.map(layer => ({ |
|
inputSize: layer.inputSize, |
|
outputSize: layer.outputSize, |
|
activation: layer.activation |
|
})) |
|
}; |
|
this.details = trainingSummary; |
|
return trainingSummary; |
|
} |
|
|
|
|
|
predict(input) { |
|
let layerInput = input; |
|
const allActivations = [input]; |
|
const allRawValues = []; |
|
for (let i = 0; i < this.weights.length; i++) { |
|
const weights = this.weights[i]; |
|
const biases = this.biases[i]; |
|
const activation = this.activations[i]; |
|
const layerOutput = []; |
|
const rawValues = []; |
|
for (let j = 0; j < weights.length; j++) { |
|
const weight = weights[j]; |
|
let sum = biases[j]; |
|
for (let k = 0; k < layerInput.length; k++) { |
|
sum += layerInput[k] * weight[k]; |
|
} |
|
rawValues.push(sum); |
|
layerOutput.push(this.activationFunction(sum, activation)); |
|
} |
|
allRawValues.push(rawValues); |
|
allActivations.push(layerOutput); |
|
layerInput = layerOutput; |
|
} |
|
this.lastActivations = allActivations; |
|
this.lastRawValues = allRawValues; |
|
return layerInput; |
|
} |
|
|
|
|
|
save(name = 'model') { |
|
const data = { |
|
weights: this.weights, |
|
biases: this.biases, |
|
activations: this.activations, |
|
layers: this.layers, |
|
details: this.details |
|
}; |
|
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); |
|
} |
|
|
|
|
|
load(callback) { |
|
const handleListener = (event) => { |
|
const file = event.target.files[0]; |
|
if (!file) return; |
|
const reader = new FileReader(); |
|
reader.onload = (event) => { |
|
const text = event.target.result; |
|
try { |
|
const data = JSON.parse(text); |
|
this.weights = data.weights; |
|
this.biases = data.biases; |
|
this.activations = data.activations; |
|
this.layers = data.layers; |
|
this.details = data.details; |
|
callback(); |
|
if (this.debug === true) console.log('Model loaded successfully!'); |
|
input.removeEventListener('change', handleListener); |
|
input.remove(); |
|
} catch (e) { |
|
input.removeEventListener('change', handleListener); |
|
input.remove(); |
|
if (this.debug === true) console.error('Failed to load model:', e); |
|
} |
|
}; |
|
reader.readAsText(file); |
|
}; |
|
const input = document.createElement('input'); |
|
input.type = 'file'; |
|
input.accept = '.json'; |
|
input.style.opacity = '0'; |
|
document.body.append(input); |
|
input.addEventListener('change', handleListener.bind(this)); |
|
input.click(); |
|
} |
|
} |
|
document.getElementById("loadDataBtn").onclick = () => { |
|
document.getElementById('trainingData').value = `1.0, 0.0, 0.0, 0.0 |
|
0.7, 0.7, 0.8, 1 |
|
0.0, 1.0, 0.0, 0.5` |
|
document.getElementById('testData').value = `0.4, 0.2, 0.6, 1.0 |
|
0.2, 0.82, 0.83, 1.0` |
|
} |
|
|
|
const nn = new carbono(); |
|
let lossHistory = []; |
|
const ctx = document.getElementById('lossGraph').getContext('2d'); |
|
|
|
function parseCSV(csv) { |
|
return csv.trim().split('\n').map(row => { |
|
const values = row.split(',').map(Number); |
|
return { |
|
input: values.slice(0, -1), |
|
output: [values[values.length - 1]] |
|
}; |
|
}); |
|
} |
|
|
|
function drawLossGraph() { |
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); |
|
const width = ctx.canvas.width; |
|
const height = ctx.canvas.height; |
|
|
|
const maxLoss = Math.max( |
|
...lossHistory.map(loss => Math.max(loss.train, loss.test || 0)) |
|
); |
|
|
|
ctx.strokeStyle = '#fff'; |
|
ctx.beginPath(); |
|
lossHistory.forEach((loss, i) => { |
|
const x = (i / (lossHistory.length - 1)) * width; |
|
const y = height - (loss.train / maxLoss) * height; |
|
if (i === 0) ctx.moveTo(x, y); |
|
else ctx.lineTo(x, y); |
|
}); |
|
ctx.stroke(); |
|
|
|
ctx.strokeStyle = '#777'; |
|
ctx.beginPath(); |
|
lossHistory.forEach((loss, i) => { |
|
if (loss.test !== undefined) { |
|
const x = (i / (lossHistory.length - 1)) * width; |
|
const y = height - (loss.test / maxLoss) * height; |
|
if (i === 0 || lossHistory[i - 1].test === undefined) ctx.moveTo(x, y); |
|
else ctx.lineTo(x, y); |
|
} |
|
}); |
|
ctx.stroke(); |
|
} |
|
|
|
function createLayerConfigUI(numLayers) { |
|
const container = document.getElementById('hiddenLayersConfig'); |
|
container.innerHTML = ''; |
|
for (let i = 0; i < numLayers; i++) { |
|
const group = document.createElement('div'); |
|
group.className = 'input-group'; |
|
const label = document.createElement('label'); |
|
label.textContent = `layer ${i + 1} nodes:`; |
|
const input = document.createElement('input'); |
|
input.type = 'number'; |
|
input.value = 5; |
|
input.dataset.layerIndex = i; |
|
const activationLabel = document.createElement('label'); |
|
activationLabel.innerHTML = `<br>activation:`; |
|
const activationSelect = document.createElement('select'); |
|
const activations = ['tanh', 'sigmoid', 'relu', 'selu']; |
|
activations.forEach(act => { |
|
const option = document.createElement('option'); |
|
option.value = act; |
|
option.textContent = act; |
|
activationSelect.appendChild(option); |
|
}); |
|
activationSelect.dataset.layerIndex = i; |
|
group.appendChild(label); |
|
group.appendChild(input); |
|
group.appendChild(activationLabel); |
|
group.appendChild(activationSelect); |
|
container.appendChild(group); |
|
} |
|
} |
|
document.getElementById('numHiddenLayers').addEventListener('change', (event) => { |
|
const numLayers = parseInt(event.target.value); |
|
createLayerConfigUI(numLayers); |
|
}); |
|
createLayerConfigUI(document.getElementById('numHiddenLayers').value); |
|
document.getElementById('trainButton').addEventListener('click', async () => { |
|
lossHistory = []; |
|
const trainingData = parseCSV(document.getElementById('trainingData').value); |
|
const testData = parseCSV(document.getElementById('testData').value); |
|
lossHistory = []; |
|
document.getElementById('stats').innerHTML = ''; |
|
const numHiddenLayers = parseInt(document.getElementById('numHiddenLayers').value); |
|
const layerConfigs = []; |
|
for (let i = 0; i < numHiddenLayers; i++) { |
|
const sizeInput = document.querySelector(`input[data-layer-index="${i}"]`); |
|
const activationSelect = document.querySelector(`select[data-layer-index="${i}"]`); |
|
layerConfigs.push({ |
|
size: parseInt(sizeInput.value), |
|
activation: activationSelect.value |
|
}); |
|
} |
|
nn.layers = []; |
|
nn.weights = []; |
|
nn.biases = []; |
|
nn.activations = []; |
|
const numInputs = trainingData[0].input.length; |
|
nn.layer(numInputs, layerConfigs[0].size, layerConfigs[0].activation); |
|
for (let i = 1; i < layerConfigs.length; i++) { |
|
nn.layer(layerConfigs[i - 1].size, layerConfigs[i].size, layerConfigs[i].activation); |
|
} |
|
nn.layer(layerConfigs[layerConfigs.length - 1].size, 1, 'tanh'); |
|
const options = { |
|
epochs: parseInt(document.getElementById('epochs').value), |
|
learningRate: parseFloat(document.getElementById('learningRate').value), |
|
batchSize: parseInt(document.getElementById('batchSize').value), |
|
printEveryEpochs: 1, |
|
testSet: testData.length > 0 ? testData : null, |
|
callback: async (epoch, trainLoss, testLoss) => { |
|
lossHistory.push({ |
|
train: trainLoss, |
|
test: testLoss |
|
}); |
|
drawLossGraph(); |
|
document.getElementById('epochBar').style.width = |
|
`${(epoch / options.epochs) * 100}%`; |
|
document.getElementById('stats').innerHTML = |
|
`<p> - current epoch: ${epoch}/${options.epochs}` + |
|
`<br> - train/val loss: ${trainLoss.toFixed(6)}` + |
|
(testLoss ? ` | ${testLoss.toFixed(6)}</p>` : ''); |
|
} |
|
} |
|
try { |
|
const trainButton = document.getElementById('trainButton'); |
|
trainButton.disabled = true; |
|
trainButton.textContent = 'training...'; |
|
|
|
const summary = await nn.train(trainingData, options); |
|
trainButton.disabled = false; |
|
trainButton.textContent = 'train'; |
|
|
|
document.getElementById('stats').innerHTML += '<strong>Model trained</strong>'; |
|
} catch (error) { |
|
console.error('Training error:', error); |
|
document.getElementById('trainButton').disabled = false; |
|
document.getElementById('trainButton').textContent = 'train'; |
|
} |
|
}); |
|
|
|
function drawNetwork() { |
|
const canvas = document.getElementById('networkGraph'); |
|
const ctx = canvas.getContext('2d'); |
|
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
if (!nn.lastActivations) return; |
|
const padding = 40; |
|
const width = canvas.width - padding * 2; |
|
const height = canvas.height - padding * 2; |
|
|
|
const layerPositions = []; |
|
|
|
const inputLayer = []; |
|
const inputX = padding; |
|
const inputSize = nn.layers[0].inputSize; |
|
for (let i = 0; i < inputSize; i++) { |
|
const inputY = padding + (height * i) / (inputSize - 1); |
|
inputLayer.push({ |
|
x: inputX, |
|
y: inputY, |
|
value: nn.lastActivations[0][i] |
|
}); |
|
} |
|
layerPositions.push(inputLayer); |
|
|
|
for (let i = 1; i < nn.lastActivations.length - 1; i++) { |
|
const layer = nn.lastActivations[i]; |
|
const layerNodes = []; |
|
const layerX = padding + (width * i) / (nn.lastActivations.length - 1); |
|
for (let j = 0; j < layer.length; j++) { |
|
const nodeY = padding + (height * j) / (layer.length - 1); |
|
layerNodes.push({ |
|
x: layerX, |
|
y: nodeY, |
|
value: layer[j] |
|
}); |
|
} |
|
layerPositions.push(layerNodes); |
|
} |
|
|
|
const outputLayer = []; |
|
const outputX = canvas.width - padding; |
|
const outputY = padding + height / 2; |
|
outputLayer.push({ |
|
x: outputX, |
|
y: outputY, |
|
value: nn.lastActivations[nn.lastActivations.length - 1][0] |
|
}); |
|
layerPositions.push(outputLayer); |
|
|
|
ctx.lineWidth = 1; |
|
for (let i = 0; i < layerPositions.length - 1; i++) { |
|
const currentLayer = layerPositions[i]; |
|
const nextLayer = layerPositions[i + 1]; |
|
const weights = nn.weights[i]; |
|
for (let j = 0; j < currentLayer.length; j++) { |
|
const nextLayerSize = nextLayer.length; |
|
for (let k = 0; k < nextLayerSize; k++) { |
|
const weight = weights[k][j]; |
|
const signal = Math.abs(currentLayer[j].value * weight); |
|
const opacity = Math.min(Math.max(signal, 0.01), 1); |
|
ctx.strokeStyle = `rgba(255, 255, 255, ${opacity})`; |
|
ctx.beginPath(); |
|
ctx.moveTo(currentLayer[j].x, currentLayer[j].y); |
|
ctx.lineTo(nextLayer[k].x, nextLayer[k].y); |
|
ctx.stroke(); |
|
} |
|
} |
|
} |
|
|
|
for (const layer of layerPositions) { |
|
for (const node of layer) { |
|
const value = Math.abs(node.value); |
|
const radius = 4; |
|
|
|
ctx.fillStyle = `rgba(255, 255, 255, ${Math.min(Math.max(value, 0.2), 1)})`; |
|
ctx.beginPath(); |
|
ctx.arc(node.x, node.y, radius, 0, Math.PI * 2); |
|
ctx.fill(); |
|
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 1.0)'; |
|
ctx.lineWidth = 1; |
|
ctx.stroke(); |
|
} |
|
} |
|
} |
|
|
|
document.getElementById('predictButton').addEventListener('click', () => { |
|
const input = document.getElementById('predictionInput').value |
|
.split(',').map(Number); |
|
const prediction = nn.predict(input); |
|
document.getElementById('predictionResult').innerHTML = |
|
`Prediction: ${prediction[0].toFixed(6)}`; |
|
drawNetwork(); |
|
}); |
|
|
|
function resizeCanvases() { |
|
const lossCanvas = document.getElementById('lossGraph'); |
|
const networkCanvas = document.getElementById('networkGraph'); |
|
lossCanvas.width = lossCanvas.parentElement.clientWidth; |
|
lossCanvas.height = lossCanvas.parentElement.clientHeight; |
|
networkCanvas.width = networkCanvas.parentElement.clientWidth; |
|
networkCanvas.height = networkCanvas.parentElement.clientHeight; |
|
drawNetwork(); |
|
} |
|
window.addEventListener('resize', resizeCanvases); |
|
resizeCanvases(); |
|
|
|
document.getElementById('saveButton').addEventListener('click', () => { |
|
nn.save('model'); |
|
}); |
|
|
|
document.getElementById('loadButton').addEventListener('click', () => { |
|
nn.load(() => { |
|
console.log('Model loaded successfully!'); |
|
|
|
document.getElementById('stats').innerHTML += '<p><strong>Model loaded successfully!</strong></p>'; |
|
}); |
|
}); |
|
</script> |
|
</body> |
|
|
|
</html> |