Here is a tiny network with two inputs, one hidden layer and one output, trying to learn a simple target. Press Step to run one forward pass (the prediction) followed by one backward pass (the error flowing back through every weight). Watch the connections glow as blame travels backward, and the error number shrink.
<p class="hint">A tiny network: 2 inputs → 2 hidden neurons → 1 output. It is trying to predict the <b>target</b> from the fixed input. Press <b>Step</b> for one forward pass (prediction) then one backward pass (error flowing back through every weight). Glowing edges carry the most blame. Watch the error shrink.</p>
<svg id="net" viewBox="0 0 320 200" aria-label="neural network"></svg>
<div class="row">
<span class="pill">prediction <b id="pred">–</b></span>
<span class="pill">target <b id="targ">0.90</b></span>
<span class="pill err">error <b id="err">–</b></span>
</div>
<div class="btns">
<button id="step" type="button">Step</button>
<button id="train" type="button">Train 200×</button>
<button id="reset" type="button" class="ghost">Reset weights</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#net { width: 100%; max-width: 420px; height: auto; display: block; margin: 0 auto; }
.edge { stroke: #9aa3ad; transition: stroke .15s, stroke-width .15s; }
.node { fill: #e8eef3; stroke: #1d3557; stroke-width: 1.5; }
.node.out { fill: #d8e8df; }
.nlabel { font: 600 11px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; }
.row { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; margin: .5rem 0; }
.pill { font-size: .85rem; background: #eef2f6; border: 1px solid #d3dce4; border-radius: 999px;
padding: .25rem .7rem; }
.pill b { font: 700 .9rem ui-monospace, monospace; color: #1d3557; }
.pill.err b { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// A 2-2-1 sigmoid network trained by BACKPROPAGATION (chain rule run backward).
// Forward: h = sig(W1.x + b1); y = sig(W2.h + b2). Loss = 1/2 (y - t)^2.
// Backward (the whole point): one sweep gives the gradient of EVERY weight.
const x = [1, 0]; // fixed input
const t = 0.90; // target
const lr = 0.5; // learning rate
const sig = z => 1 / (1 + Math.exp(-z));
let W1, b1, W2, b2; // W1[j][i] hidden j from input i; W2[j] output from hidden j
function reset() {
W1 = [[0.30, -0.20], [-0.10, 0.40]];
b1 = [0.0, 0.0];
W2 = [0.20, -0.30];
b2 = 0.0;
lastGrad = { W1: [[0,0],[0,0]], W2: [0,0] };
forward(); draw(); setReadout();
}
let h = [0, 0], y = 0, lastGrad;
function forward() {
for (let j = 0; j < 2; j++) h[j] = sig(W1[j][0]*x[0] + W1[j][1]*x[1] + b1[j]);
y = sig(W2[0]*h[0] + W2[1]*h[1] + b2);
}
// One backward pass: chain rule from the loss back to every weight.
function backward() {
const dL_dy = (y - t); // d loss / d y
const dy_dz = y * (1 - y); // sigmoid derivative at output
const dOut = dL_dy * dy_dz; // "blame" at the output neuron
const gW2 = [dOut * h[0], dOut * h[1]];
const gb2 = dOut;
const gW1 = [[0,0],[0,0]], gb1 = [0,0];
for (let j = 0; j < 2; j++) {
const dh = dOut * W2[j] * h[j] * (1 - h[j]); // blame pushed back to hidden j
gW1[j][0] = dh * x[0];
gW1[j][1] = dh * x[1];
gb1[j] = dh;
}
// gradient descent step
for (let j = 0; j < 2; j++) {
W2[j] -= lr * gW2[j];
for (let i = 0; i < 2; i++) W1[j][i] -= lr * gW1[j][i];
b1[j] -= lr * gb1[j];
}
b2 -= lr * gb2;
lastGrad = { W1: gW1, W2: gW2 };
}
function step() { forward(); backward(); forward(); draw(); setReadout(); }
function train() { for (let k = 0; k < 200; k++) { forward(); backward(); } forward(); draw(); setReadout(); }
function setReadout() {
const err = 0.5 * (y - t) * (y - t);
document.getElementById('pred').textContent = y.toFixed(3);
document.getElementById('err').textContent = err.toFixed(4);
}
// ── drawing ──────────────────────────────────────────────────────────
const IN = [[40,60],[40,140]], HID = [[160,55],[160,145]], OUT = [[280,100]];
function edgeWidth(g) { return 1 + Math.min(6, Math.abs(g) * 40); }
function edgeColor(g) { const a = Math.min(1, Math.abs(g) * 6);
return g >= 0 ? `rgba(230,57,70,${0.25+0.75*a})` : `rgba(29,53,87,${0.25+0.75*a})`; }
function draw() {
const svg = document.getElementById('net'); let s = '';
for (let j = 0; j < 2; j++) for (let i = 0; i < 2; i++) {
const g = lastGrad.W1[j][i];
s += `<line class="edge" x1="${IN[i][0]}" y1="${IN[i][1]}" x2="${HID[j][0]}" y2="${HID[j][1]}" stroke="${edgeColor(g)}" stroke-width="${edgeWidth(g)}"/>`;
}
for (let j = 0; j < 2; j++) {
const g = lastGrad.W2[j];
s += `<line class="edge" x1="${HID[j][0]}" y1="${HID[j][1]}" x2="${OUT[0][0]}" y2="${OUT[0][1]}" stroke="${edgeColor(g)}" stroke-width="${edgeWidth(g)}"/>`;
}
const node = (p, lbl, cls='') => `<circle class="node ${cls}" cx="${p[0]}" cy="${p[1]}" r="16"/><text class="nlabel" x="${p[0]}" y="${p[1]+4}">${lbl}</text>`;
s += node(IN[0], x[0]); s += node(IN[1], x[1]);
s += node(HID[0], h[0].toFixed(2)); s += node(HID[1], h[1].toFixed(2));
s += node(OUT[0], y.toFixed(2), 'out');
svg.innerHTML = s;
}
document.getElementById('step').addEventListener('click', step);
document.getElementById('train').addEventListener('click', train);
document.getElementById('reset').addEventListener('click', reset);
reset();
<p class="hint">A tiny network: 2 inputs → 2 hidden neurons → 1 output. It is trying to predict the <b>target</b> from the fixed input. Press <b>Step</b> for one forward pass (prediction) then one backward pass (error flowing back through every weight). Glowing edges carry the most blame. Watch the error shrink.</p>
<svg id="net" viewBox="0 0 320 200" aria-label="neural network"></svg>
<div class="row">
<span class="pill">prediction <b id="pred">–</b></span>
<span class="pill">target <b id="targ">0.90</b></span>
<span class="pill err">error <b id="err">–</b></span>
</div>
<div class="btns">
<button id="step" type="button">Step</button>
<button id="train" type="button">Train 200×</button>
<button id="reset" type="button" class="ghost">Reset weights</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#net { width: 100%; max-width: 420px; height: auto; display: block; margin: 0 auto; }
.edge { stroke: #9aa3ad; transition: stroke .15s, stroke-width .15s; }
.node { fill: #e8eef3; stroke: #1d3557; stroke-width: 1.5; }
.node.out { fill: #d8e8df; }
.nlabel { font: 600 11px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; }
.row { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; margin: .5rem 0; }
.pill { font-size: .85rem; background: #eef2f6; border: 1px solid #d3dce4; border-radius: 999px;
padding: .25rem .7rem; }
.pill b { font: 700 .9rem ui-monospace, monospace; color: #1d3557; }
.pill.err b { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// A 2-2-1 sigmoid network trained by BACKPROPAGATION (chain rule run backward).
// Forward: h = sig(W1.x + b1); y = sig(W2.h + b2). Loss = 1/2 (y - t)^2.
// Backward (the whole point): one sweep gives the gradient of EVERY weight.
const x = [1, 0]; // fixed input
const t = 0.90; // target
const lr = 0.5; // learning rate
const sig = z => 1 / (1 + Math.exp(-z));
let W1, b1, W2, b2; // W1[j][i] hidden j from input i; W2[j] output from hidden j
function reset() {
W1 = [[0.30, -0.20], [-0.10, 0.40]];
b1 = [0.0, 0.0];
W2 = [0.20, -0.30];
b2 = 0.0;
lastGrad = { W1: [[0,0],[0,0]], W2: [0,0] };
forward(); draw(); setReadout();
}
let h = [0, 0], y = 0, lastGrad;
function forward() {
for (let j = 0; j < 2; j++) h[j] = sig(W1[j][0]*x[0] + W1[j][1]*x[1] + b1[j]);
y = sig(W2[0]*h[0] + W2[1]*h[1] + b2);
}
// One backward pass: chain rule from the loss back to every weight.
function backward() {
const dL_dy = (y - t); // d loss / d y
const dy_dz = y * (1 - y); // sigmoid derivative at output
const dOut = dL_dy * dy_dz; // "blame" at the output neuron
const gW2 = [dOut * h[0], dOut * h[1]];
const gb2 = dOut;
const gW1 = [[0,0],[0,0]], gb1 = [0,0];
for (let j = 0; j < 2; j++) {
const dh = dOut * W2[j] * h[j] * (1 - h[j]); // blame pushed back to hidden j
gW1[j][0] = dh * x[0];
gW1[j][1] = dh * x[1];
gb1[j] = dh;
}
// gradient descent step
for (let j = 0; j < 2; j++) {
W2[j] -= lr * gW2[j];
for (let i = 0; i < 2; i++) W1[j][i] -= lr * gW1[j][i];
b1[j] -= lr * gb1[j];
}
b2 -= lr * gb2;
lastGrad = { W1: gW1, W2: gW2 };
}
function step() { forward(); backward(); forward(); draw(); setReadout(); }
function train() { for (let k = 0; k < 200; k++) { forward(); backward(); } forward(); draw(); setReadout(); }
function setReadout() {
const err = 0.5 * (y - t) * (y - t);
document.getElementById('pred').textContent = y.toFixed(3);
document.getElementById('err').textContent = err.toFixed(4);
}
// ── drawing ──────────────────────────────────────────────────────────
const IN = [[40,60],[40,140]], HID = [[160,55],[160,145]], OUT = [[280,100]];
function edgeWidth(g) { return 1 + Math.min(6, Math.abs(g) * 40); }
function edgeColor(g) { const a = Math.min(1, Math.abs(g) * 6);
return g >= 0 ? `rgba(230,57,70,${0.25+0.75*a})` : `rgba(29,53,87,${0.25+0.75*a})`; }
function draw() {
const svg = document.getElementById('net'); let s = '';
for (let j = 0; j < 2; j++) for (let i = 0; i < 2; i++) {
const g = lastGrad.W1[j][i];
s += `<line class="edge" x1="${IN[i][0]}" y1="${IN[i][1]}" x2="${HID[j][0]}" y2="${HID[j][1]}" stroke="${edgeColor(g)}" stroke-width="${edgeWidth(g)}"/>`;
}
for (let j = 0; j < 2; j++) {
const g = lastGrad.W2[j];
s += `<line class="edge" x1="${HID[j][0]}" y1="${HID[j][1]}" x2="${OUT[0][0]}" y2="${OUT[0][1]}" stroke="${edgeColor(g)}" stroke-width="${edgeWidth(g)}"/>`;
}
const node = (p, lbl, cls='') => `<circle class="node ${cls}" cx="${p[0]}" cy="${p[1]}" r="16"/><text class="nlabel" x="${p[0]}" y="${p[1]+4}">${lbl}</text>`;
s += node(IN[0], x[0]); s += node(IN[1], x[1]);
s += node(HID[0], h[0].toFixed(2)); s += node(HID[1], h[1].toFixed(2));
s += node(OUT[0], y.toFixed(2), 'out');
svg.innerHTML = s;
}
document.getElementById('step').addEventListener('click', step);
document.getElementById('train').addEventListener('click', train);
document.getElementById('reset').addEventListener('click', reset);
reset();
Notice what backpropagation buys you. The forward pass just computes a prediction. The backward pass reuses those same numbers to hand every single weight its own gradient — its personal "move me this way" instruction — in one sweep, no matter how many weights there are. Press Train 200x and watch the error collapse as the gradients are followed by gradient descent.
Comments
Loading comments...