|
<!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 ReinforcementModule { |
|
constructor(network, options = {}) { |
|
this.network = network; |
|
this.options = { |
|
memorySize: options.memorySize || 128, |
|
batchSize: options.batchSize || 16, |
|
learningRate: options.learningRate || 0.01, |
|
gamma: options.gamma || 0.9, |
|
epsilon: options.epsilon || 1, |
|
epsilonMin: options.epsilonMin || 0.01, |
|
epsilonDecay: options.epsilonDecay || 0.95, |
|
weightUpdateRange: options.weightUpdateRange || 0.02, |
|
actionSpace: options.actionSpace || 2048, |
|
memoryLayerSize: options.memoryLayerSize || 32, |
|
predictionHorizon: options.predictionHorizon || 16, |
|
memoryCellDecay: options.memoryCellDecay || 0.9 |
|
}; |
|
|
|
|
|
this.memoryCells = { |
|
shortTerm: new Array(this.options.memoryLayerSize).fill(0), |
|
longTerm: new Array(this.options.memoryLayerSize).fill(0), |
|
cellState: new Array(this.options.memoryLayerSize).fill(0) |
|
}; |
|
|
|
|
|
this.gates = { |
|
forget: this.createGateNetwork(this.options.memoryLayerSize), |
|
input: this.createGateNetwork(this.options.memoryLayerSize), |
|
output: this.createGateNetwork(this.options.memoryLayerSize), |
|
candidates: this.createGateNetwork(this.options.memoryLayerSize) |
|
}; |
|
|
|
this.memory = []; |
|
this.currentState = this.getNetworkState(); |
|
this.bestWeights = this.cloneWeights(network.weights); |
|
this.bestLoss = Infinity; |
|
this.epsilon = this.options.epsilon; |
|
|
|
this.qNetwork = this.createQNetwork(); |
|
this.outcomePredictor = this.createOutcomePredictor(); |
|
} |
|
|
|
createGateNetwork(size) { |
|
const gate = new carbono(false); |
|
gate.layer(this.getFlattenedStateSize(), size, "sigmoid"); |
|
return gate; |
|
} |
|
|
|
createQNetwork() { |
|
const qNet = new carbono(false); |
|
const stateSize = this.getFlattenedStateSize(); |
|
const actionSize = this.getActionSpaceSize(); |
|
|
|
qNet.layer(stateSize + actionSize, 16, "selu"); |
|
qNet.layer(16, 16, "selu"); |
|
qNet.layer(16, 1, "selu"); |
|
|
|
return qNet; |
|
} |
|
|
|
createOutcomePredictor() { |
|
const predictor = new carbono(false); |
|
const inputSize = |
|
this.getFlattenedStateSize() + this.options.memoryLayerSize * 3; |
|
|
|
predictor.layer(inputSize, 8, "tanh"); |
|
predictor.layer(8, 8, "tanh"); |
|
predictor.layer(8, this.options.predictionHorizon, "tanh"); |
|
|
|
return predictor; |
|
} |
|
|
|
getFlattenedStateSize() { |
|
let size = 0; |
|
this.network.weights.forEach((layer) => { |
|
size += layer.flat().length; |
|
}); |
|
return size + 3; |
|
} |
|
|
|
getActionSpaceSize() { |
|
let size = 0; |
|
this.network.weights.forEach((layer) => { |
|
size += layer.flat().length * this.options.actionSpace; |
|
}); |
|
return size; |
|
} |
|
|
|
getNetworkState() { |
|
const flatWeights = this.network.weights |
|
.map((layer) => layer.flat()) |
|
.flat(); |
|
return [...flatWeights, this.bestLoss, this.getCurrentLoss(), this.epsilon]; |
|
} |
|
|
|
async getCurrentLoss() { |
|
let totalLoss = 0; |
|
for (const data of this.network.trainingData) { |
|
const prediction = this.network.predict(data.input); |
|
totalLoss += Math.abs(prediction[0] - data.output[0]); |
|
} |
|
return totalLoss / this.network.trainingData.length; |
|
} |
|
|
|
async updateMemoryCells(state) { |
|
const forgetGate = this.gates.forget.predict(state); |
|
const inputGate = this.gates.input.predict(state); |
|
const outputGate = this.gates.output.predict(state); |
|
const candidates = this.gates.candidates.predict(state); |
|
|
|
for (let i = 0; i < this.options.memoryLayerSize; i++) { |
|
this.memoryCells.cellState[i] *= forgetGate[i]; |
|
this.memoryCells.cellState[i] += inputGate[i] * candidates[i]; |
|
this.memoryCells.shortTerm[i] = |
|
Math.tanh(this.memoryCells.cellState[i]) * outputGate[i]; |
|
this.memoryCells.longTerm[i] = |
|
this.memoryCells.longTerm[i] * this.options.memoryCellDecay + |
|
this.memoryCells.shortTerm[i] * (1 - this.options.memoryCellDecay); |
|
} |
|
} |
|
|
|
async predictOutcomes(state) { |
|
const input = [ |
|
...state, |
|
...this.memoryCells.shortTerm, |
|
...this.memoryCells.longTerm, |
|
...this.memoryCells.cellState |
|
]; |
|
return this.outcomePredictor.predict(input); |
|
} |
|
|
|
encodeAction(action) { |
|
const encoded = new Array(this.getActionSpaceSize()).fill(0); |
|
encoded[action] = 1; |
|
return encoded; |
|
} |
|
|
|
async predictQValue(state, action) { |
|
const encoded = this.encodeAction(action); |
|
const input = [...state, ...encoded]; |
|
const qValue = this.qNetwork.predict(input); |
|
return qValue[0]; |
|
} |
|
|
|
simulateAction(state, action) { |
|
const simState = [...state]; |
|
const updates = this.actionToWeightUpdates(action); |
|
let stateIndex = 0; |
|
|
|
for (const layer of updates) { |
|
for (const row of layer) { |
|
for (const update of row) { |
|
simState[stateIndex] += update; |
|
stateIndex++; |
|
} |
|
} |
|
} |
|
|
|
return simState; |
|
} |
|
|
|
async selectAction() { |
|
if (Math.random() < this.epsilon) { |
|
return Math.floor(Math.random() * this.getActionSpaceSize()); |
|
} |
|
|
|
const state = this.getNetworkState(); |
|
await this.updateMemoryCells(state); |
|
|
|
let bestAction = 0; |
|
let bestOutcome = -Infinity; |
|
|
|
for (let action = 0; action < this.getActionSpaceSize(); action++) { |
|
const simState = this.simulateAction(state, action); |
|
const outcomes = await this.predictOutcomes(simState); |
|
|
|
const expectedValue = outcomes.reduce((sum, val, i) => { |
|
return sum + val * Math.pow(this.options.gamma, i); |
|
}, 0); |
|
|
|
if (expectedValue > bestOutcome) { |
|
bestOutcome = expectedValue; |
|
bestAction = action; |
|
} |
|
} |
|
|
|
return bestAction; |
|
} |
|
|
|
actionToWeightUpdates(action) { |
|
const updates = []; |
|
let actionIndex = action; |
|
|
|
for (const layer of this.network.weights) { |
|
const layerUpdate = []; |
|
for (let i = 0; i < layer.length; i++) { |
|
const rowUpdate = []; |
|
for (let j = 0; j < layer[i].length; j++) { |
|
const actionValue = actionIndex % this.options.actionSpace; |
|
actionIndex = Math.floor(actionIndex / this.options.actionSpace); |
|
const update = |
|
((actionValue / (this.options.actionSpace - 1)) * 2 - 1) * |
|
this.options.weightUpdateRange; |
|
rowUpdate.push(update); |
|
} |
|
layerUpdate.push(rowUpdate); |
|
} |
|
updates.push(layerUpdate); |
|
} |
|
|
|
return updates; |
|
} |
|
|
|
async applyAction(action) { |
|
const updates = this.actionToWeightUpdates(action); |
|
for (let i = 0; i < this.network.weights.length; i++) { |
|
for (let j = 0; j < this.network.weights[i].length; j++) { |
|
for (let k = 0; k < this.network.weights[i][j].length; k++) { |
|
this.network.weights[i][j][k] += updates[i][j][k]; |
|
} |
|
} |
|
} |
|
} |
|
|
|
calculateReward(oldLoss, newLoss) { |
|
const improvement = oldLoss - newLoss; |
|
const bestReward = newLoss < this.bestLoss ? 1.0 : 0.0; |
|
return improvement + bestReward; |
|
} |
|
|
|
async getActualOutcomes(state, steps) { |
|
const outcomes = []; |
|
let currentState = state; |
|
|
|
for (let i = 0; i < steps; i++) { |
|
const loss = await this.getCurrentLoss(); |
|
outcomes.push(loss); |
|
const action = await this.selectAction(); |
|
currentState = this.simulateAction(currentState, action); |
|
} |
|
|
|
return outcomes; |
|
} |
|
|
|
async trainOutcomePredictor(experience) { |
|
const { state, nextState } = experience; |
|
const actualOutcomes = await this.getActualOutcomes( |
|
nextState, |
|
this.options.predictionHorizon |
|
); |
|
|
|
const input = [ |
|
...state, |
|
...this.memoryCells.shortTerm, |
|
...this.memoryCells.longTerm, |
|
...this.memoryCells.cellState |
|
]; |
|
|
|
await this.outcomePredictor.train( |
|
[ |
|
{ |
|
input: input, |
|
output: actualOutcomes |
|
} |
|
], |
|
{ |
|
epochs: 10, |
|
learningRate: this.options.learningRate |
|
} |
|
); |
|
} |
|
|
|
async trainQNetwork(batch) { |
|
for (const experience of batch) { |
|
const { state, action, reward, nextState } = experience; |
|
const currentQ = await this.predictQValue(state, action); |
|
|
|
let maxNextQ = -Infinity; |
|
for (let a = 0; a < this.getActionSpaceSize(); a++) { |
|
const nextQ = await this.predictQValue(nextState, a); |
|
maxNextQ = Math.max(maxNextQ, nextQ); |
|
} |
|
|
|
const targetQ = reward + this.options.gamma * maxNextQ; |
|
const input = [...state, ...this.encodeAction(action)]; |
|
|
|
await this.qNetwork.train( |
|
[ |
|
{ |
|
input: input, |
|
output: [targetQ] |
|
} |
|
], |
|
{ |
|
epochs: 10, |
|
learningRate: this.options.learningRate |
|
} |
|
); |
|
} |
|
} |
|
|
|
async update(currentLoss) { |
|
const state = this.getNetworkState(); |
|
const action = await this.selectAction(); |
|
await this.applyAction(action); |
|
const nextState = this.getNetworkState(); |
|
const newLoss = await this.getCurrentLoss(); |
|
const reward = this.calculateReward(currentLoss, newLoss); |
|
|
|
const experience = { |
|
state, |
|
action, |
|
reward, |
|
nextState |
|
}; |
|
|
|
this.memory.push(experience); |
|
await this.trainOutcomePredictor(experience); |
|
|
|
if (this.memory.length > this.options.memorySize) { |
|
this.memory.shift(); |
|
} |
|
|
|
if (this.memory.length >= this.options.batchSize) { |
|
const batch = []; |
|
for (let i = 0; i < this.options.batchSize; i++) { |
|
const index = Math.floor(Math.random() * this.memory.length); |
|
batch.push(this.memory[index]); |
|
} |
|
await this.trainQNetwork(batch); |
|
} |
|
|
|
if (newLoss < this.bestLoss) { |
|
this.bestLoss = newLoss; |
|
this.bestWeights = this.cloneWeights(this.network.weights); |
|
} |
|
|
|
this.epsilon = Math.max( |
|
this.options.epsilonMin, |
|
this.epsilon * this.options.epsilonDecay |
|
); |
|
|
|
return { |
|
loss: newLoss, |
|
bestLoss: this.bestLoss, |
|
epsilon: this.epsilon |
|
}; |
|
} |
|
|
|
cloneWeights(weights) { |
|
return weights.map((layer) => layer.map((row) => [...row])); |
|
} |
|
} |
|
|
|
|
|
|
|
class carbono { |
|
constructor(debug = true) { |
|
this.layers = []; |
|
this.weights = []; |
|
this.biases = []; |
|
this.activations = []; |
|
this.details = {}; |
|
this.debug = debug; |
|
} |
|
|
|
|
|
play(options = {}) { |
|
console.log("Reinforcement Learning Activated"); |
|
this.rl = new ReinforcementModule(this, options); |
|
return this.rl; |
|
} |
|
|
|
|
|
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." |
|
); |
|
} |
|
} |
|
|
|
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 (this.rl) { |
|
this.rl.update(lastTrainLoss); |
|
} |
|
|
|
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...'; |
|
nn.play() |
|
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> |