carbono / index.html
appvoid's picture
Update index.html
e236ca7 verified
raw
history blame
42.1 kB
<!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);
}
}
/* Responsive Design */
@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">
<!-- Group 1: Data & Training -->
<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>
<!-- New UI Elements for Layer Configuration -->
<div id="hiddenLayersConfig"></div>
</div>
<!-- Group 2: Progress & Visualization -->
<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
};
// Initialize memory cells
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)
};
// Initialize gates and networks
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]));
}
}
// 🧠 carbono: A Fun and Friendly Neural Network Class 🧠
// This micro-library wraps everything you need to have
// This is the simplest yet functional feedforward mlp in js
class carbono {
constructor(debug = true) {
this.layers = []; // 📚 Stores info about each layer
this.weights = []; // ⚖️ Stores weights for each layer
this.biases = []; // 🔧 Stores biases for each layer
this.activations = []; // 🚀 Stores activation functions for each layer
this.details = {}; // 📊 Stores details about the model
this.debug = debug; // 🐛 Enables or disables debug messages
}
// 🎮 Initialize reinforcement learning module
play(options = {}) {
console.log("Reinforcement Learning Activated");
this.rl = new ReinforcementModule(this, options);
return this.rl;
}
// 🏗️ Add a new layer to the neural network
layer(inputSize, outputSize, activation = "tanh") {
// 🧱 Store layer information
this.layers.push({
inputSize,
outputSize,
activation
});
// 🔍 Check if the new layer's input size matches the previous layer's output size
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."
);
}
}
// 🎲 Initialize weights using Xavier/Glorot initialization
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);
// 🎚️ Initialize biases with small positive values
const biases = Array(outputSize).fill(0.01);
this.biases.push(biases);
// 🚀 Store the activation function for this layer
this.activations.push(activation);
}
// 🧮 Apply the activation function
activationFunction(x, activation) {
switch (activation) {
case "tanh":
return Math.tanh(x); // 〰️ Hyperbolic tangent
case "sigmoid":
return 1 / (1 + Math.exp(-x)); // 📈 S-shaped curve
case "relu":
return Math.max(0, x); // 📐 Rectified Linear Unit
case "selu":
const alpha = 1.67326;
const scale = 1.0507;
return x > 0 ? scale * x : scale * alpha * (Math.exp(x) - 1); // 🚀 Scaled Exponential Linear Unit
default:
throw new Error("Whoops! We don't know that activation function.");
}
}
// 📐 Calculate the derivative of the 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."
);
}
}
// 🏋️‍♀️ Train the neural network
async train(trainSet, options = {}) {
// 🎛️ Set up training options with default values
const {
epochs = 200, // 🔄 Number of times to go through the entire dataset
learningRate = 0.212, // 📏 How big of steps to take when adjusting weights
batchSize = 16, // 📦 Number of samples to process before updating weights
printEveryEpochs = 100, // 🖨️ How often to print progress
earlyStopThreshold = 1e-6, // 🛑 When to stop if the error is small enough
testSet = null, // 🧪 Optional test set for evaluation
callback = null // 📡 Callback function for real-time updates
} = options;
const start = Date.now(); // ⏱️ Start the timer
// 🛡️ Make sure batch size is at least 2
if (batchSize < 1) batchSize = 2;
// 🏗️ Automatically create layers if none exist
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;
// 🔄 Main training loop
for (let epoch = 0; epoch < epochs; epoch++) {
let trainError = 0;
// 📦 Process data in batches
for (let b = 0; b < trainSet.length; b += batchSize) {
const batch = trainSet.slice(b, b + batchSize);
let batchError = 0;
// 🧠 Forward pass and backward pass for each item in the batch
for (const data of batch) {
// 🏃‍♂️ Forward pass
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);
}
// 🔙 Backward pass
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);
}
// 🔧 Update weights and biases
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]); // Assuming binary output
}
trainError += batchError;
}
lastTrainLoss = trainError / trainSet.length;
// 🎮 Apply reinforcement learning if initialized
if (this.rl) {
this.rl.update(lastTrainLoss);
}
// 🧪 Evaluate on test set if provided
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;
}
// 📢 Print progress if needed
if ((epoch + 1) % printEveryEpochs === 0 && this.debug === true) {
console.log(
`Epoch ${epoch + 1}, Train Loss: ${lastTrainLoss.toFixed(6)}${
testSet ? `, Test Loss: ${lastTestLoss.toFixed(6)}` : ""
}`
);
}
// 📡 Call the callback function with current progress
if (callback) {
await callback(epoch + 1, lastTrainLoss, lastTestLoss);
}
// Add a small delay to prevent UI freezing
await new Promise((resolve) => setTimeout(resolve, 0));
// 🛑 Check for early stopping
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(); // ⏱️ Stop the timer
// 🧮 Calculate total number of parameters
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;
}
// 📊 Create a summary of the training
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;
}
// 🔮 Use the trained network to make predictions
predict(input) {
let layerInput = input;
const allActivations = [input]; // Track all activations through layers
const allRawValues = []; // Track pre-activation values
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;
}
// Store last activation values for visualization
this.lastActivations = allActivations;
this.lastRawValues = allRawValues;
return layerInput;
}
// 💾 Save the model to a file
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 a saved model from a file
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`
}
// Interface code
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;
// Combine train and test losses to find overall max for scaling
const maxLoss = Math.max(
...lossHistory.map(loss => Math.max(loss.train, loss.test || 0))
);
// Draw training loss (white line)
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();
// Draw test loss (gray line)
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 = ''; // Clear previous UI
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 = []; // Initialize as empty array
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 = []; // Reset 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'); // Output layer
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';
// Display final summary
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; // Don't draw if no predictions made yet
const padding = 40;
const width = canvas.width - padding * 2;
const height = canvas.height - padding * 2;
// Calculate node positions
const layerPositions = [];
// Add input layer explicitly
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);
// Add hidden layers
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);
}
// Add output layer explicitly
const outputLayer = [];
const outputX = canvas.width - padding;
const outputY = padding + height / 2; // Center the output node
outputLayer.push({
x: outputX,
y: outputY,
value: nn.lastActivations[nn.lastActivations.length - 1][0]
});
layerPositions.push(outputLayer);
// Draw connections
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();
}
}
}
// Draw nodes
for (const layer of layerPositions) {
for (const node of layer) {
const value = Math.abs(node.value);
const radius = 4;
// Node fill
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();
// Node border
ctx.strokeStyle = 'rgba(255, 255, 255, 1.0)';
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
// Modify the predict button event listener
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(); // Draw the network visualization
});
// Add network canvas resize handling
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(); // Redraw network when canvas is resized
}
window.addEventListener('resize', resizeCanvases);
resizeCanvases();
// Save button functionality
document.getElementById('saveButton').addEventListener('click', () => {
nn.save('model');
});
// Load button functionality
document.getElementById('loadButton').addEventListener('click', () => {
nn.load(() => {
console.log('Model loaded successfully!');
// Optionally, you can add a message to the UI indicating that the model has been loaded
document.getElementById('stats').innerHTML += '<p><strong>Model loaded successfully!</strong></p>';
});
});
</script>
</body>
</html>