Skip to content

Instantly share code, notes, and snippets.

@alksily
Last active May 27, 2026 09:37
Show Gist options
  • Select an option

  • Save alksily/88d0d7a6f36d70c3851f4e060b52dcbd to your computer and use it in GitHub Desktop.

Select an option

Save alksily/88d0d7a6f36d70c3851f4e060b52dcbd to your computer and use it in GitHub Desktop.
Neural network in just 300 lines of JavaScript
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Live Demo</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;700&family=Space+Grotesk:wght@300;500;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #0a0a0f;
--panel: #111118;
--border: #1e1e2e;
--green: #00ff88;
--blue: #4488ff;
--text: #c8c8d8;
--dim: #555568;
--accent: #ff4488;
}
body {
background: var(--bg);
color: var(--text);
font-family: 'JetBrains Mono', monospace;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
user-select: none;
}
header {
display: flex;
align-items: center;
gap: 24px;
padding: 14px 24px;
border-bottom: 1px solid var(--border);
background: var(--panel);
flex-shrink: 0;
}
.logo {
font-family: 'Space Grotesk', sans-serif;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.08em;
color: #fff;
}
.logo span {
color: var(--green);
}
.stat {
font-size: 11px;
color: var(--dim);
}
.stat b {
color: var(--text);
font-weight: 400;
}
.hint {
margin-left: auto;
font-size: 11px;
color: var(--dim);
display: flex;
gap: 20px;
}
.hint span {
display: flex;
align-items: center;
gap: 6px;
}
.dot-hint {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
main {
display: flex;
flex: 1;
overflow: hidden;
}
#canvas {
flex: 1;
cursor: crosshair;
display: block;
}
aside {
width: 220px;
border-left: 1px solid var(--border);
background: var(--panel);
display: flex;
flex-direction: column;
padding: 16px;
gap: 20px;
flex-shrink: 0;
overflow-y: auto;
}
.section-label {
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--dim);
margin-bottom: 8px;
}
.loss-display {
font-size: 28px;
font-weight: 700;
color: var(--green);
font-variant-numeric: tabular-nums;
line-height: 1;
}
.loss-display.bad {
color: var(--accent);
}
#loss-chart {
width: 100%;
height: 60px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
}
.network-viz {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
height: 100px;
}
.layer-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex: 1;
}
.neuron {
width: 14px;
height: 14px;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--bg);
transition: background 0.1s;
flex-shrink: 0;
}
.layer-name {
font-size: 9px;
color: var(--dim);
margin-top: 4px;
text-align: center;
}
button {
background: none;
border: 1px solid var(--border);
color: var(--text);
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s;
width: 100%;
text-align: left;
}
button:hover {
border-color: var(--green);
color: var(--green);
}
button.danger:hover {
border-color: var(--accent);
color: var(--accent);
}
.iter-count {
font-size: 11px;
color: var(--dim);
font-variant-numeric: tabular-nums;
}
.iter-count b {
color: var(--text);
}
.speed-row {
display: flex;
align-items: center;
gap: 10px;
}
input[type=range] {
flex: 1;
accent-color: var(--green);
cursor: pointer;
}
.speed-label {
font-size: 11px;
color: var(--dim);
width: 28px;
text-align: right;
}
.points-count {
display: flex;
gap: 12px;
font-size: 12px;
}
.pc-green {
color: var(--green);
}
.pc-blue {
color: var(--blue);
}
</style>
</head>
<body>
<header>
<div class="logo">Live<span>Demo</span></div>
<div class="stat">architecture: <b>2 → 6 → 6 → 2</b></div>
<div class="stat">activation: <b>sigmoid</b></div>
<div class="stat">lr: <b>0.05</b></div>
<div class="hint">
<span><span class="dot-hint" style="background:var(--green)"></span>ЛКМ — зелёная</span>
<span><span class="dot-hint" style="background:var(--blue)"></span>ПКМ — синяя</span>
</div>
</header>
<main>
<canvas id="canvas"></canvas>
<aside>
<div>
<div class="section-label">Loss</div>
<div class="loss-display" id="loss-val">—</div>
<canvas id="loss-chart"></canvas>
</div>
<div>
<div class="section-label">Итерации</div>
<div class="iter-count">всего: <b id="iter-total">0</b></div>
<div class="iter-count">за кадр: <b id="iter-frame">500</b></div>
<div class="speed-row" style="margin-top:8px">
<input type="range" id="speed" min="100" max="5000" value="500" step="100">
<div class="speed-label" id="speed-label">500</div>
</div>
</div>
<div>
<div class="section-label">Точки</div>
<div class="points-count">
<span class="pc-green" id="cnt-green">● 0</span>
<span class="pc-blue" id="cnt-blue">● 0</span>
</div>
</div>
<div>
<div class="section-label">Сеть</div>
<div class="network-viz" id="net-viz"></div>
</div>
<div style="display:flex;flex-direction:column;gap:8px;margin-top:auto">
<button id="btn-reset-nn">↺ сбросить сеть</button>
<button id="btn-clear" class="danger">✕ очистить точки</button>
</div>
</aside>
</main>
<script>
const sigmoid = x => 1 / (1 + Math.exp(-x))
const dsigmoid = y => y * (1 - y)
class NeuralNetwork {
constructor(lr, ...sizes) {
this.lr = lr
this.sizes = sizes
this.init()
}
init() {
this.weights = []
this.biases = []
for (let i = 0; i < this.sizes.length - 1; i++) {
const rows = this.sizes[i], cols = this.sizes[i + 1]
this.weights.push(Array.from({length: rows}, () =>
Array.from({length: cols}, () => Math.random() * 2 - 1)))
this.biases.push(Array.from({length: cols}, () => Math.random() * 2 - 1))
}
}
feedForward(inputs) {
this.activations = [inputs.slice()]
let cur = inputs.slice()
for (let l = 0; l < this.weights.length; l++) {
const next = []
for (let j = 0; j < this.sizes[l + 1]; j++) {
let sum = this.biases[l][j]
for (let k = 0; k < this.sizes[l]; k++) sum += cur[k] * this.weights[l][k][j]
next.push(sigmoid(sum))
}
cur = next
this.activations.push(cur)
}
return cur
}
backprop(targets) {
const out = this.activations[this.activations.length - 1]
let errors = targets.map((t, i) => t - out[i])
for (let l = this.weights.length - 1; l >= 0; l--) {
const cur = this.activations[l + 1]
const prev = this.activations[l]
const grads = cur.map((a, i) => errors[i] * dsigmoid(a) * this.lr)
const nextErrors = Array(this.sizes[l]).fill(0)
for (let k = 0; k < this.sizes[l]; k++) {
for (let j = 0; j < this.sizes[l + 1]; j++) {
nextErrors[k] += this.weights[l][k][j] * errors[j]
this.weights[l][k][j] += grads[j] * prev[k]
}
}
for (let j = 0; j < this.sizes[l + 1]; j++) this.biases[l][j] += grads[j]
errors = nextErrors
}
}
}
let nn = new NeuralNetwork(0.05, 2, 6, 6, 2)
let points = [] // {x, y, type}
let iterTotal = 0
let itersPerFrame = 500
let lossHistory = []
const MAX_LOSS_HISTORY = 80
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const lossChart = document.getElementById('loss-chart')
const lossCtx = lossChart.getContext('2d')
function resize() {
const rect = canvas.parentElement.getBoundingClientRect()
canvas.width = rect.width - 220
canvas.height = rect.height
lossChart.width = lossChart.offsetWidth
lossChart.height = lossChart.offsetHeight
}
resize()
window.addEventListener('resize', resize)
const RES = 3 // pixel size of each cell
function drawBackground() {
const W = canvas.width, H = canvas.height
const iw = Math.ceil(W / RES), ih = Math.ceil(H / RES)
const imgData = ctx.createImageData(iw, ih)
for (let i = 0; i < iw; i++) {
for (let j = 0; j < ih; j++) {
const nx = i / iw - 0.5
const ny = j / ih - 0.5
const out = nn.feedForward([nx, ny])
const g = out[0], b = out[1]
const t = g / (g + b + 1e-9) // how green (0=blue, 1=green)
const idx = (j * iw + i) * 4
// green zone: dark teal. blue zone: dark navy
imgData.data[idx] = Math.round(10 + t * 5)
imgData.data[idx + 1] = Math.round(20 + t * 60)
imgData.data[idx + 2] = Math.round(40 + (1 - t) * 80)
imgData.data[idx + 3] = 255
}
}
// Draw scaled
const tmp = document.createElement('canvas')
tmp.width = iw
tmp.height = ih
tmp.getContext('2d').putImageData(imgData, 0, 0)
ctx.drawImage(tmp, 0, 0, W, H)
}
function drawPoints() {
for (const p of points) {
const col = p.type === 0 ? '#00ff88' : '#4488ff'
ctx.beginPath()
ctx.arc(p.x, p.y, 9, 0, Math.PI * 2)
ctx.fillStyle = col + '22'
ctx.fill()
ctx.beginPath()
ctx.arc(p.x, p.y, 6, 0, Math.PI * 2)
ctx.fillStyle = col
ctx.fill()
ctx.strokeStyle = '#fff4'
ctx.lineWidth = 1
ctx.stroke()
}
}
function drawBoundary() {
const W = canvas.width, H = canvas.height
const STEP = 6
ctx.strokeStyle = 'rgba(255,255,255,0.25)'
ctx.lineWidth = 1
for (let i = 0; i < W; i += STEP) {
for (let j = 0; j < H; j += STEP) {
const nx = i / W - 0.5, ny = j / H - 0.5
const o = nn.feedForward([nx, ny])
const diff = o[0] - o[1]
const nx2 = (i + STEP) / W - 0.5, ny2 = (j + STEP) / H - 0.5
const o2 = nn.feedForward([nx2, ny2])
const diff2 = o2[0] - o2[1]
if (Math.sign(diff) !== Math.sign(diff2)) {
ctx.beginPath()
ctx.moveTo(i, j)
ctx.lineTo(i + STEP, j + STEP)
ctx.stroke()
}
}
}
}
function drawLossChart() {
const W = lossChart.width, H = lossChart.height
lossCtx.clearRect(0, 0, W, H)
if (lossHistory.length < 2) return
const max = Math.max(...lossHistory, 0.01)
lossCtx.beginPath()
lossCtx.strokeStyle = '#00ff8888'
lossCtx.lineWidth = 1.5
for (let i = 0; i < lossHistory.length; i++) {
const x = (i / (MAX_LOSS_HISTORY - 1)) * W
const y = H - (lossHistory[i] / max) * (H - 4) - 2
i === 0 ? lossCtx.moveTo(x, y) : lossCtx.lineTo(x, y)
}
lossCtx.stroke()
// fill
lossCtx.lineTo(W, H)
lossCtx.lineTo(0, H)
lossCtx.fillStyle = '#00ff8811'
lossCtx.fill()
}
function updateNetViz() {
const viz = document.getElementById('net-viz')
const acts = nn.activations
if (!acts) return
viz.innerHTML = ''
const names = ['вход', 'скрытый', 'скрытый', 'выход']
acts.forEach((layer, li) => {
const col = document.createElement('div')
col.className = 'layer-col'
const show = layer.slice(0, 6)
show.forEach(val => {
const n = document.createElement('div')
n.className = 'neuron'
const v = Math.max(0, Math.min(1, val))
n.style.background = `rgba(0, 255, 136, ${v * 0.9})`
n.style.borderColor = `rgba(0, 255, 136, ${0.1 + v * 0.5})`
col.appendChild(n)
})
const lbl = document.createElement('div')
lbl.className = 'layer-name'
lbl.textContent = names[li] || ''
col.appendChild(lbl)
viz.appendChild(col)
})
}
let lastLossUpdate = 0
function train() {
if (points.length === 0) return
let lossSum = 0
const n = itersPerFrame
for (let i = 0; i < n; i++) {
const p = points[Math.floor(Math.random() * points.length)]
const nx = p.x / canvas.width - 0.5
const ny = p.y / canvas.height - 0.5
const out = nn.feedForward([nx, ny])
const targets = p.type === 0 ? [1, 0] : [0, 1]
lossSum += targets.reduce((s, t, i) => s + (t - out[i]) ** 2, 0)
nn.backprop(targets)
}
iterTotal += n
const loss = lossSum / n
lastLossUpdate++
if (lastLossUpdate >= 3) {
lossHistory.push(loss)
if (lossHistory.length > MAX_LOSS_HISTORY) lossHistory.shift()
lastLossUpdate = 0
}
// Update stats
const lossEl = document.getElementById('loss-val')
lossEl.textContent = loss.toFixed(4)
lossEl.className = 'loss-display' + (loss > 0.2 ? ' bad' : '')
document.getElementById('iter-total').textContent = iterTotal.toLocaleString()
}
function render() {
train()
drawBackground()
drawBoundary()
drawPoints()
updateNetViz()
drawLossChart()
requestAnimationFrame(render)
}
render()
function getPos(e) {
const r = canvas.getBoundingClientRect()
return {x: e.clientX - r.left, y: e.clientY - r.top}
}
canvas.addEventListener('mousedown', e => {
e.preventDefault()
const {x, y} = getPos(e)
const type = e.button === 2 ? 1 : 0
points.push({x, y, type})
updateCounts()
})
canvas.addEventListener('mousemove', e => {
if (e.buttons === 1 || e.buttons === 2) {
const {x, y} = getPos(e)
const type = e.buttons === 2 ? 1 : 0
points.push({x, y, type})
updateCounts()
}
})
canvas.addEventListener('contextmenu', e => e.preventDefault())
function updateCounts() {
const g = points.filter(p => p.type === 0).length
const b = points.filter(p => p.type === 1).length
document.getElementById('cnt-green').textContent = `● ${g}`
document.getElementById('cnt-blue').textContent = `● ${b}`
}
document.getElementById('speed').addEventListener('input', function() {
itersPerFrame = +this.value
document.getElementById('speed-label').textContent = this.value
document.getElementById('iter-frame').textContent = this.value
})
document.getElementById('btn-reset-nn').addEventListener('click', () => {
nn.init()
iterTotal = 0
lossHistory = []
document.getElementById('iter-total').textContent = '0'
document.getElementById('loss-val').textContent = '—'
})
document.getElementById('btn-clear').addEventListener('click', () => {
points = []
nn.init()
iterTotal = 0
lossHistory = []
document.getElementById('iter-total').textContent = '0'
document.getElementById('loss-val').textContent = '—'
updateCounts()
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment