|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="utf-8" /> |
|
|
<title>Carbono UI — Minimal Mono</title> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
|
|
<style> |
|
|
:root{ |
|
|
--fg:#fff;--bg:#000;--muted:#8a8a8a;--panel:#0e0e0e;--line:#222;--ink:#111;--ink2:#1a1a1a; |
|
|
--radius:6px;--pad:10px;--fs:12px;--lh:1.25;--gap:10px; |
|
|
--base-w:1200; |
|
|
--base-h:680; |
|
|
} |
|
|
|
|
|
*{box-sizing:border-box;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} |
|
|
html,body{height:100%;background:var(--bg);color:var(--fg);margin:0} |
|
|
a{color:var(--fg);text-decoration:none;border-bottom:1px solid transparent} |
|
|
a:hover{border-bottom-color:var(--fg)} |
|
|
|
|
|
body{display:flex;align-items:center;justify-content:center;overflow:hidden} |
|
|
#fit{ |
|
|
width:calc(var(--base-w)*1px); |
|
|
height:calc(var(--base-h)*1px); |
|
|
transform-origin: center; |
|
|
display:flex;flex-direction:column;gap:var(--gap); |
|
|
font:500 var(--fs)/var(--lh) ui-monospace,SFMono-Regular,Menlo,Consolas,Monaco,monospace; |
|
|
letter-spacing:.1px;font-variant-numeric:tabular-nums; |
|
|
} |
|
|
|
|
|
.top{ |
|
|
display:grid;grid-template-columns:1fr auto auto;gap:var(--gap); |
|
|
align-items:center;padding:6px 8px;border:1px solid var(--line);border-radius:var(--radius);background:var(--panel) |
|
|
} |
|
|
.brand{text-transform:uppercase;letter-spacing:.5px;font-weight:700} |
|
|
.muted{color:var(--muted)} |
|
|
.chip{background:var(--fg);color:var(--bg);padding:2px 6px;border-radius:999px;cursor:pointer;user-select:none} |
|
|
|
|
|
.grid{ |
|
|
display:grid;gap:var(--gap); |
|
|
grid-template-columns: 3.5fr 3.5fr 3fr; |
|
|
grid-template-rows: 1fr; |
|
|
height:100%; |
|
|
} |
|
|
.panel{ |
|
|
display:flex;flex-direction:column;gap:var(--gap); |
|
|
border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(180deg,var(--panel),var(--ink)); |
|
|
padding:var(--pad); |
|
|
} |
|
|
.block{border:1px solid var(--line);border-radius:var(--radius);background:var(--ink2);padding:8px} |
|
|
.title{ |
|
|
font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin:0 0 6px 0; |
|
|
padding-bottom:6px;border-bottom:1px solid var(--line) |
|
|
} |
|
|
|
|
|
label{display:block;margin-bottom:4px;color:var(--muted)} |
|
|
.row{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--gap)} |
|
|
.input,textarea,select{ |
|
|
width:100%;background:#141414;border:1px solid var(--line);color:var(--fg); |
|
|
padding:6px;border-radius:4px;outline:none;transition:border-color .15s ease; |
|
|
} |
|
|
.input:focus,textarea:focus,select:focus{border-color:#fff} |
|
|
textarea{resize:none} |
|
|
|
|
|
.btns{display:flex;gap:6px;flex-wrap:wrap} |
|
|
button{ |
|
|
height:26px;line-height:24px;padding:0 10px;border-radius:4px;border:1px solid #fff;background:#fff;color:#000; |
|
|
cursor:pointer;transition:filter .15s ease |
|
|
} |
|
|
button:hover{filter:brightness(.9)} |
|
|
button:disabled{opacity:.6;cursor:not-allowed;filter:none} |
|
|
|
|
|
.canvas-wrap{height:170px;border:1px solid var(--line);border-radius:4px;position:relative;background:#0a0a0a} |
|
|
canvas{position:absolute;inset:0;width:100%;height:100%} |
|
|
|
|
|
.bar{height:4px;border-radius:999px;background:#161616;overflow:hidden} |
|
|
.bar>i{display:block;height:100%;width:0;background:#fff;transition:width .2s linear} |
|
|
|
|
|
.hint{color:var(--muted);margin:4px 0 0 0} |
|
|
.stat{display:grid;grid-template-columns:auto 1fr;gap:6px 10px} |
|
|
.stat b{color:#fff} |
|
|
|
|
|
.pred{display:grid;grid-template-columns:1fr auto;gap:6px;align-items:center} |
|
|
|
|
|
.panel,.block{overflow:hidden} |
|
|
@media (max-width:900px){ |
|
|
|
|
|
.grid{grid-template-columns:1fr} |
|
|
} |
|
|
@media (prefers-reduced-motion:reduce){ |
|
|
*{animation:none!important;transition:none!important} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div id="fit"> |
|
|
<div class="top"> |
|
|
<div class="brand">Carbono Playground</div> |
|
|
<div class="muted">Learning. Simple. Visual.</div> |
|
|
<span id="loadDataBtn" class="chip">Load sample</span> |
|
|
</div> |
|
|
|
|
|
<div class="grid"> |
|
|
|
|
|
<section class="panel" aria-label="Model Settings"> |
|
|
<h4 class="title">Model</h4> |
|
|
<div class="block"> |
|
|
<label>Training Set</label> |
|
|
<textarea id="trainingData" rows="3" placeholder="1,1,1,0 1,0,1,0 0,1,0,1"></textarea> |
|
|
<div class="hint">Last number is target.</div> |
|
|
</div> |
|
|
<div class="row"> |
|
|
<div class="block"> |
|
|
<label>Epochs</label> |
|
|
<input class="input" type="number" id="epochs" value="50" /> |
|
|
</div> |
|
|
<div class="block"> |
|
|
<label>LR</label> |
|
|
<input class="input" type="number" id="learningRate" value="0.1" step="0.001" /> |
|
|
</div> |
|
|
<div class="block"> |
|
|
<label>Batch</label> |
|
|
<input class="input" type="number" id="batchSize" value="8" /> |
|
|
</div> |
|
|
<div class="block"> |
|
|
<label>Hidden</label> |
|
|
<input class="input" type="number" id="numHiddenLayers" value="1" min="1" max="4" /> |
|
|
</div> |
|
|
</div> |
|
|
<div id="hiddenLayersConfig" class="block"></div> |
|
|
<div class="block"> |
|
|
<label>Validation Set</label> |
|
|
<textarea id="testData" rows="2" placeholder="0,0,0,1"></textarea> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section class="panel" aria-label="Training & Visualization"> |
|
|
<h4 class="title">Training</h4> |
|
|
<div class="canvas-wrap"><canvas id="lossGraph"></canvas></div> |
|
|
<div class="hint">Loss: white = train, gray = val.</div> |
|
|
<div class="bar"><i id="epochBar"></i></div> |
|
|
<div id="stats" class="block stat"></div> |
|
|
|
|
|
<h4 class="title">Visualization</h4> |
|
|
<div class="canvas-wrap"><canvas id="networkGraph"></canvas></div> |
|
|
<div class="hint">Internal representation.</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section class="panel" aria-label="Control"> |
|
|
<h4 class="title">Control</h4> |
|
|
<div class="block btns"> |
|
|
<button id="trainButton">Train</button> |
|
|
<button id="saveButton">Save</button> |
|
|
<button id="loadButton">Load</button> |
|
|
</div> |
|
|
|
|
|
<h4 class="title">Predict</h4> |
|
|
<div class="block pred"> |
|
|
<input class="input" type="text" id="predictionInput" placeholder="0.4, 0.2, 0.6" /> |
|
|
<button id="predictButton">Predict</button> |
|
|
</div> |
|
|
<div id="predictionResult" class="block"></div> |
|
|
|
|
|
<div class="block"> |
|
|
<div class="hint">Repo: <a href="https://github.com/appvoid/carbono" target="_blank" rel="noopener">github/appvoid/carbono</a></div> |
|
|
</div> |
|
|
</section> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
(function(){ |
|
|
const baseW = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--base-w'),10); |
|
|
const baseH = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--base-h'),10); |
|
|
const fitEl = document.getElementById('fit'); |
|
|
function fit(){ const s = Math.min(window.innerWidth/baseW, window.innerHeight/baseH); fitEl.style.transform = `scale(${s})`; } |
|
|
window.addEventListener('resize', fit, {passive:true}); fit(); |
|
|
})(); |
|
|
|
|
|
|
|
|
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 lastOut = this.layers[this.layers.length-2].outputSize; |
|
|
if(inputSize!==lastOut) throw new Error('Input size must match previous layer output size.'); |
|
|
} |
|
|
const W=[]; for(let i=0;i<outputSize;i++){ const row=[]; for(let j=0;j<inputSize;j++){ row.push((Math.random()-0.5)*2*Math.sqrt(6/(inputSize+outputSize))); } W.push(row); } |
|
|
this.weights.push(W); this.biases.push(Array(outputSize).fill(0.01)); this.activations.push(activation); |
|
|
} |
|
|
activationFunction(x,a){ switch(a){case 'tanh':return Math.tanh(x);case 'sigmoid':return 1/(1+Math.exp(-x));case 'relu':return Math.max(0,x);case 'selu':{const alpha=1.67326,scale=1.0507;return x>0?scale*x:scale*alpha*(Math.exp(x)-1);}default:throw new Error('Unknown activation');} } |
|
|
activationDerivative(x,a){ switch(a){case 'tanh':return 1-Math.pow(Math.tanh(x),2);case 'sigmoid':{const s=1/(1+Math.exp(-x));return s*(1-s);}case 'relu':return x>0?1:0;case 'selu':{const alpha=1.67326,scale=1.0507;return x>0?scale:scale*alpha*Math.exp(x);}default:throw new Error('Unknown derivative');} } |
|
|
positionalEncoding(input,maxLen){ const pe=new Array(maxLen).fill(0).map((_,pos)=>new Array(input[0].length).fill(0).map((_,i)=>{const ang=pos/Math.pow(10000,2*i/input[0].length);return pos%2===0?Math.sin(ang):Math.cos(ang);})); return input.map((seq,idx)=>seq.map((v,i)=>v+pe[idx][i])); } |
|
|
multiHeadSelfAttention(input,numHeads=2){ |
|
|
const headSize=input[0].length/numHeads; const heads=[...Array(numHeads)].map(()=>[...Array(input.length)].map(()=>[...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 scores=[...Array(numHeads)].map(()=>[...Array(input.length)].map(()=>[...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 s=0; for(let k=0;k<headSize;k++) s+=heads[h][i][k]*heads[h][j][k]; scores[h][i][j]=s; } |
|
|
const weights=scores.map(head=>head.map(row=>{const ex=row.map(v=>Math.exp(v)); const sum=ex.reduce((a,b)=>a+b,0); return ex.map(v=>v/sum)})); |
|
|
const out=[...Array(input.length)].map(()=>[...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++) out[i][h*headSize+j]+=weights[h][i][k]*heads[h][k][j]; |
|
|
return out; |
|
|
} |
|
|
layerNormalization(arr){ const m=arr.reduce((s,v)=>s+v,0)/arr.length; const v=arr.reduce((s,x)=>s+Math.pow(x-m,2),0)/arr.length; return arr.map(x=>(x-m)/Math.sqrt(v+1e-5)); } |
|
|
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(); const batch=Math.max(1,batchSize); |
|
|
if(this.layers.length===0){ const n=trainSet[0].input.length; this.layer(n,n,'tanh'); this.layer(n,1,'tanh'); } |
|
|
let lastTrainLoss=0,lastTestLoss=null; |
|
|
for(let epoch=0;epoch<epochs;epoch++){ |
|
|
let trainError=0; |
|
|
for(let b=0;b<trainSet.length;b+=batch){ |
|
|
const batchItems=trainSet.slice(b,b+batch); let batchError=0; |
|
|
for(const data of batchItems){ |
|
|
const L=[data.input]; |
|
|
for(let i=0;i<this.weights.length;i++){ |
|
|
const inputs=L[i], W=this.weights[i], B=this.biases[i], act=this.activations[i]; const out=[]; |
|
|
for(let j=0;j<W.length;j++){ const w=W[j]; let sum=B[j]; for(let k=0;k<inputs.length;k++) sum+=inputs[k]*w[k]; out.push(this.activationFunction(sum,act)); } |
|
|
L.push(out); |
|
|
} |
|
|
const outIn=L[L.length-1]; const outErr=[]; for(let i=0;i<outIn.length;i++) outErr.push((data.output[i]??0)-outIn[i]); |
|
|
let layerErrors=[outErr]; |
|
|
for(let i=this.weights.length-2;i>=0;i--){ |
|
|
const Wnext=this.weights[i+1], nextErr=layerErrors[0], curIn=L[i+1], act=this.activations[i]; const errs=[]; |
|
|
for(let j=0;j<this.layers[i].outputSize;j++){ let e=0; for(let k=0;k<this.layers[i+1].outputSize;k++) e+=nextErr[k]*Wnext[k][j]; errs.push(e*this.activationDerivative(curIn[j],act)); } |
|
|
layerErrors.unshift(errs); |
|
|
} |
|
|
for(let i=0;i<this.weights.length;i++){ |
|
|
const inputs=L[i], errs=layerErrors[i], W=this.weights[i], B=this.biases[i]; |
|
|
for(let j=0;j<W.length;j++){ const w=W[j]; for(let k=0;k<inputs.length;k++) w[k]+=learningRate*errs[j]*inputs[k]; B[j]+=learningRate*errs[j]; } |
|
|
} |
|
|
batchError+=Math.abs(outErr[0]??0); |
|
|
} |
|
|
trainError+=batchError; |
|
|
} |
|
|
lastTrainLoss=trainError/trainSet.length; |
|
|
if(testSet){ let te=0; for(const d of testSet){ const p=this.predict(d.input); te+=Math.abs((d.output[0]??0)-(p[0]??0)); } lastTestLoss=te/testSet.length; } |
|
|
if((epoch+1)%printEveryEpochs===0 && this.debug) console.log(`Epoch ${epoch+1} | Train ${lastTrainLoss.toFixed(6)}${testSet?` | Val ${lastTestLoss.toFixed(6)}`:''}`); |
|
|
if(callback) await callback(epoch+1,lastTrainLoss,lastTestLoss); |
|
|
await new Promise(r=>setTimeout(r,0)); |
|
|
if(lastTrainLoss<earlyStopThreshold) { if(this.debug) console.log(`Early stop @${epoch+1}`); break; } |
|
|
} |
|
|
const end=Date.now(); let params=0; for(let i=0;i<this.weights.length;i++){ params+=this.weights[i].flat().length+this.biases[i].length; } |
|
|
const summary={trainLoss:lastTrainLoss,testLoss:lastTestLoss,parameters:params,training:{time:end-start,epochs,learningRate,batchSize:batch},layers:this.layers.map(l=>({inputSize:l.inputSize,outputSize:l.outputSize,activation:l.activation}))}; |
|
|
this.details=summary; return summary; |
|
|
} |
|
|
predict(input){ |
|
|
let x=input; const acts=[input], raw=[]; |
|
|
for(let i=0;i<this.weights.length;i++){ const W=this.weights[i], B=this.biases[i], a=this.activations[i]; const y=[], r=[]; |
|
|
for(let j=0;j<W.length;j++){ const w=W[j]; let s=B[j]; for(let k=0;k<x.length;k++) s+=x[k]*w[k]; r.push(s); y.push(this.activationFunction(s,a)); } |
|
|
raw.push(r); acts.push(y); x=y; |
|
|
} |
|
|
this.lastActivations=acts; this.lastRawValues=raw; return x; |
|
|
} |
|
|
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 onChange=(e)=>{ const f=e.target.files[0]; if(!f) return; const r=new FileReader(); |
|
|
r.onload=(ev)=>{ try{ const data=JSON.parse(ev.target.result); this.weights=data.weights; this.biases=data.biases; this.activations=data.activations; this.layers=data.layers; this.details=data.details; callback&&callback(); if(this.debug) console.log('Loaded'); }catch(err){ if(this.debug) console.error('Load failed',err); } finally{ input.removeEventListener('change',onChange); input.remove(); } }; |
|
|
r.readAsText(f); |
|
|
}; |
|
|
const input=document.createElement('input'); input.type='file'; input.accept='.json'; input.style.position='fixed'; input.style.opacity='0'; document.body.append(input); input.addEventListener('change',onChange); input.click(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded',()=>{ |
|
|
const nn=new carbono(); |
|
|
let lossHistory=[]; |
|
|
|
|
|
const lossCanvas=document.getElementById('lossGraph'); |
|
|
const networkCanvas=document.getElementById('networkGraph'); |
|
|
const lossCtx=lossCanvas.getContext('2d'); |
|
|
|
|
|
const el={ |
|
|
loadDataBtn:document.getElementById('loadDataBtn'), |
|
|
trainingData:document.getElementById('trainingData'), |
|
|
testData:document.getElementById('testData'), |
|
|
numHiddenLayers:document.getElementById('numHiddenLayers'), |
|
|
hiddenLayersConfig:document.getElementById('hiddenLayersConfig'), |
|
|
trainButton:document.getElementById('trainButton'), |
|
|
stats:document.getElementById('stats'), |
|
|
epochBar:document.getElementById('epochBar'), |
|
|
epochs:document.getElementById('epochs'), |
|
|
learningRate:document.getElementById('learningRate'), |
|
|
batchSize:document.getElementById('batchSize'), |
|
|
predictButton:document.getElementById('predictButton'), |
|
|
predictionInput:document.getElementById('predictionInput'), |
|
|
predictionResult:document.getElementById('predictionResult'), |
|
|
saveButton:document.getElementById('saveButton'), |
|
|
loadButton:document.getElementById('loadButton') |
|
|
}; |
|
|
|
|
|
const parseCSV=(csv)=> csv.trim().split('\n').filter(Boolean).map(row=>{ |
|
|
const values=row.split(',').map(s=>Number(s.trim())); |
|
|
return {input:values.slice(0,-1),output:[values[values.length-1]]}; |
|
|
}); |
|
|
|
|
|
function drawLossGraph(){ |
|
|
const {width,height}=lossCanvas; |
|
|
lossCtx.clearRect(0,0,width,height); |
|
|
if(lossHistory.length===0) return; |
|
|
const maxLoss=Math.max(1e-9,...lossHistory.map(l=>Math.max(l.train, l.test??0))); |
|
|
function line(data, color){ |
|
|
lossCtx.strokeStyle=color; lossCtx.beginPath(); |
|
|
data.forEach((v,i)=>{ const x=(i/(data.length-1))*width; const y=height-(v/maxLoss)*height; if(i===0) lossCtx.moveTo(x,y); else lossCtx.lineTo(x,y); }); |
|
|
lossCtx.stroke(); |
|
|
} |
|
|
line(lossHistory.map(l=>l.train),'#ffffff'); |
|
|
if(lossHistory.some(l=>l.test!==undefined)) line(lossHistory.map(l=>l.test ?? 0),'#777777'); |
|
|
} |
|
|
|
|
|
function createLayerConfigUI(n){ |
|
|
el.hiddenLayersConfig.innerHTML=''; |
|
|
for(let i=0;i<n;i++){ |
|
|
const block=document.createElement('div'); |
|
|
block.className='row'; |
|
|
block.style.marginTop='0'; |
|
|
block.innerHTML=` |
|
|
<div style="grid-column: span 2;" class="block"> |
|
|
<label>Layer ${i+1} Nodes</label> |
|
|
<input class="input" type="number" value="5" data-layer-index="${i}"> |
|
|
</div> |
|
|
<div style="grid-column: span 2;" class="block"> |
|
|
<label>Activation</label> |
|
|
<select class="input" data-layer-index="${i}"> |
|
|
<option>tanh</option> |
|
|
<option>sigmoid</option> |
|
|
<option>relu</option> |
|
|
<option>selu</option> |
|
|
</select> |
|
|
</div>`; |
|
|
el.hiddenLayersConfig.appendChild(block); |
|
|
} |
|
|
} |
|
|
|
|
|
async function trainModel(){ |
|
|
lossHistory=[]; |
|
|
const trainingData=parseCSV(el.trainingData.value); |
|
|
const testData=parseCSV(el.testData.value||''); |
|
|
el.stats.innerHTML=''; |
|
|
|
|
|
const nHidden=parseInt(el.numHiddenLayers.value,10); |
|
|
const layerCfg=[]; |
|
|
for(let i=0;i<nHidden;i++){ |
|
|
const size=parseInt(document.querySelector(`input[data-layer-index="${i}"]`).value,10); |
|
|
const act=document.querySelector(`select[data-layer-index="${i}"]`).value; |
|
|
layerCfg.push({size,activation:act}); |
|
|
} |
|
|
|
|
|
nn.layers=[]; nn.weights=[]; nn.biases=[]; nn.activations=[]; |
|
|
const numInputs=trainingData[0].input.length; |
|
|
nn.layer(numInputs, layerCfg[0].size, layerCfg[0].activation); |
|
|
for(let i=1;i<layerCfg.length;i++) nn.layer(layerCfg[i-1].size, layerCfg[i].size, layerCfg[i].activation); |
|
|
nn.layer(layerCfg[layerCfg.length-1].size, 1, 'tanh'); |
|
|
|
|
|
const opts={ |
|
|
epochs:parseInt(el.epochs.value,10), |
|
|
learningRate:parseFloat(el.learningRate.value), |
|
|
batchSize:parseInt(el.batchSize.value,10), |
|
|
printEveryEpochs:1, |
|
|
testSet:testData.length?testData:null, |
|
|
callback:async (epoch,trainLoss,testLoss)=>{ |
|
|
lossHistory.push({train:trainLoss,test:testLoss}); |
|
|
drawLossGraph(); |
|
|
el.epochBar.style.width=`${(epoch/opts.epochs)*100}%`; |
|
|
el.stats.innerHTML=` |
|
|
<div><b>Epoch</b></div><div>${epoch}/${opts.epochs}</div> |
|
|
<div><b>Train</b></div><div>${trainLoss.toFixed(6)}</div> |
|
|
${testLoss!==null?`<div><b>Val</b></div><div>${testLoss.toFixed(6)}</div>`:''} |
|
|
`; |
|
|
} |
|
|
}; |
|
|
|
|
|
try{ el.trainButton.disabled=true; el.trainButton.textContent='Training…'; await nn.train(trainingData,opts); |
|
|
el.stats.innerHTML+=`<div><b>Status</b></div><div>Model trained</div>`; |
|
|
}catch(e){ console.error('Training error:',e); |
|
|
el.stats.innerHTML+=`<div><b>Error</b></div><div>${e.message}</div>`; |
|
|
}finally{ el.trainButton.disabled=false; el.trainButton.textContent='Train'; } |
|
|
} |
|
|
|
|
|
function drawNetwork(){ |
|
|
const ctx=networkCanvas.getContext('2d'); |
|
|
ctx.clearRect(0,0,networkCanvas.width,networkCanvas.height); |
|
|
if(!nn.lastActivations) return; |
|
|
|
|
|
const pad=34; const W=networkCanvas.width-pad*2; const H=networkCanvas.height-pad*2; |
|
|
const layers=[]; |
|
|
|
|
|
|
|
|
const inSize=nn.layers[0].inputSize; const inX=pad; const inNodes=[]; |
|
|
for(let i=0;i<inSize;i++){ const y=pad+(inSize>1?(H*i)/(inSize-1):H/2); inNodes.push({x:inX,y,val:nn.lastActivations[0][i]||0}); } |
|
|
layers.push(inNodes); |
|
|
|
|
|
|
|
|
for(let i=1;i<nn.lastActivations.length-1;i++){ |
|
|
const L=nn.lastActivations[i]; const nodes=[]; const x=pad+(W*i)/(nn.lastActivations.length-1); |
|
|
for(let j=0;j<L.length;j++){ const y=pad+(L.length>1?(H*j)/(L.length-1):H/2); nodes.push({x,y,val:L[j]}); } |
|
|
layers.push(nodes); |
|
|
} |
|
|
|
|
|
|
|
|
const outX=networkCanvas.width-pad; const outY=pad+H/2; layers.push([{x:outX,y:outY,val:nn.lastActivations.at(-1)[0]||0}]); |
|
|
|
|
|
|
|
|
ctx.lineWidth=1; |
|
|
for(let i=0;i<layers.length-1;i++){ |
|
|
const A=layers[i], B=layers[i+1], Wmat=nn.weights[i]; |
|
|
for(let j=0;j<A.length;j++) for(let k=0;k<B.length;k++){ |
|
|
const w=Wmat[k][j]; const sig=Math.abs((A[j].val||0)*w); const op=Math.min(Math.max(sig,0.06),1); |
|
|
ctx.strokeStyle=`rgba(255,255,255,${op})`; ctx.beginPath(); ctx.moveTo(A[j].x,A[j].y); ctx.lineTo(B[k].x,B[k].y); ctx.stroke(); |
|
|
} |
|
|
} |
|
|
|
|
|
for(const L of layers){ for(const n of L){ const r=3.5, op=Math.min(Math.max(Math.abs(n.val),0.3),1); |
|
|
ctx.fillStyle=`rgba(255,255,255,${op})`; ctx.beginPath(); ctx.arc(n.x,n.y,r,0,Math.PI*2); ctx.fill(); |
|
|
ctx.strokeStyle='rgba(255,255,255,1)'; ctx.lineWidth=.8; ctx.stroke(); |
|
|
}} |
|
|
} |
|
|
|
|
|
function sizeCanvases(){ |
|
|
[lossCanvas,networkCanvas].forEach(cv=>{ cv.width=cv.parentElement.clientWidth; cv.height=cv.parentElement.clientHeight; }); |
|
|
drawNetwork(); |
|
|
} |
|
|
|
|
|
el.loadDataBtn.onclick=()=>{ el.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`; el.testData.value=`0.4, 0.2, 0.6, 1.0 |
|
|
0.2, 0.82, 0.83, 1.0`; }; |
|
|
|
|
|
el.numHiddenLayers.addEventListener('change',(e)=>createLayerConfigUI(parseInt(e.target.value,10))); |
|
|
el.trainButton.addEventListener('click',trainModel); |
|
|
el.predictButton.addEventListener('click',()=>{ |
|
|
const input=el.predictionInput.value.split(',').map(s=>Number(s.trim())).filter(n=>!Number.isNaN(n)); |
|
|
const p=nn.predict(input); |
|
|
el.predictionResult.textContent=`Prediction: ${Number.isFinite(p[0])?p[0].toFixed(6):'NaN'}`; |
|
|
drawNetwork(); |
|
|
}); |
|
|
el.saveButton.addEventListener('click',()=>nn.save('model')); |
|
|
el.loadButton.addEventListener('click',()=>nn.load(()=>{ el.stats.innerHTML+=`<div><b>Status</b></div><div>Model loaded</div>`; })); |
|
|
|
|
|
window.addEventListener('resize', sizeCanvases, {passive:true}); |
|
|
|
|
|
createLayerConfigUI(parseInt(el.numHiddenLayers.value,10)); |
|
|
sizeCanvases(); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |