The demo below builds the expression (x · w + b)² — a tiny neural-network neuron squared — and computes its gradient with respect to every variable using reverse-mode autodiff.
<div class="controls">
<label>x = <input id="xval" type="number" value="2" step="0.5"></label>
<label>w = <input id="wval" type="number" value="3" step="0.5"></label>
<label>b = <input id="bval" type="number" value="-1" step="0.5"></label>
</div>
<div class="btns">
<button id="fwd" type="button">Run forward pass</button>
<button id="bwd" type="button" disabled>Run backward pass</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="graph" class="graph"></div>
<p class="hint">Expression: <b>L = (x·w + b)²</b> — gradients show ∂L/∂node (exact, not approximated).</p>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .6rem; }
.controls label { font-size: .9rem; }
.controls input { width: 64px; padding: .2rem .4rem; border: 1px solid #aaa; border-radius: 6px;
font-size: .9rem; margin-left: .3rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
.hint { font-size: .85rem; color: #555; margin-top: .6rem; }
.graph { display: flex; flex-direction: column; gap: 6px; }
.row { display: flex; gap: 6px; flex-wrap: wrap; }
.node { background: #e8eef3; border: 1.5px solid #cdd9e3; border-radius: 10px;
padding: .35rem .6rem; min-width: 90px; text-align: center; font-size: .82rem; line-height: 1.4; }
.node .op { font-weight: 700; font-size: .78rem; color: #1d3557; }
.node .val { font-family: ui-monospace, monospace; font-size: .88rem; }
.node .grad { font-size: .78rem; color: #0a7d33; font-family: ui-monospace, monospace; }
.node.input { background: #dceefb; border-color: #8ab4cc; }
.node.output { background: #d4edda; border-color: #6abf80; }
.node.active { border-color: #e63946; }
.arrow { font-size: 1.1rem; color: #888; align-self: center; }
// Reverse-mode automatic differentiation (backpropagation)
// for the expression L = (x*w + b)^2
//
// We build a computation graph node by node. Each node stores:
// val — forward value
// grad — accumulated gradient ∂L/∂this (set by backward pass)
// backward() — closure that propagates grad to inputs
//
// This is exactly how PyTorch's autograd works at its core.
let nodes = []; // all nodes in creation order
let rootNode = null;
let forwardDone = false;
function makeNode(name, op, inputs) {
const n = { name, op, inputs, val: 0, grad: 0, backward: () => {} };
nodes.push(n);
return n;
}
// Operations: each returns a new node and registers a backward closure
function add(a, b, name) {
const out = makeNode(name, '+', [a, b]);
out.val = a.val + b.val;
out.backward = () => {
a.grad += out.grad; // ∂(a+b)/∂a = 1
b.grad += out.grad; // ∂(a+b)/∂b = 1
};
return out;
}
function mul(a, b, name) {
const out = makeNode(name, '×', [a, b]);
out.val = a.val * b.val;
out.backward = () => {
a.grad += b.val * out.grad; // ∂(a·b)/∂a = b
b.grad += a.val * out.grad; // ∂(a·b)/∂b = a
};
return out;
}
function square(a, name) {
const out = makeNode(name, '²', [a]);
out.val = a.val * a.val;
out.backward = () => {
a.grad += 2 * a.val * out.grad; // ∂(a²)/∂a = 2a
};
return out;
}
function runForward() {
nodes = [];
const xv = parseFloat(document.getElementById('xval').value) || 0;
const wv = parseFloat(document.getElementById('wval').value) || 0;
const bv = parseFloat(document.getElementById('bval').value) || 0;
const x = makeNode('x', 'input', []); x.val = xv;
const w = makeNode('w', 'input', []); w.val = wv;
const b = makeNode('b', 'input', []); b.val = bv;
const xw = mul(x, w, 'x·w');
const s = add(xw, b, 'x·w+b');
const L = square(s, 'L');
rootNode = L;
forwardDone = true;
document.getElementById('bwd').disabled = false;
render(false);
}
function runBackward() {
if (!forwardDone) return;
// Reset grads
nodes.forEach(n => { n.grad = 0; });
rootNode.grad = 1.0; // ∂L/∂L = 1
// Reverse topological order = just reverse creation order for this DAG
for (let i = nodes.length - 1; i >= 0; i--) {
nodes[i].backward();
}
render(true);
}
function fmt(v) {
return (Math.round(v * 10000) / 10000).toString();
}
function render(showGrad) {
const g = document.getElementById('graph');
g.innerHTML = '';
// Group nodes into rows: inputs, intermediate, output
const inputNodes = nodes.filter(n => n.inputs.length === 0);
const midNodes = nodes.filter(n => n.inputs.length > 0 && n !== rootNode);
const outNodes = [rootNode];
function makeRow(group, cls) {
if (!group || !group.length) return;
const row = document.createElement('div');
row.className = 'row';
group.forEach(n => {
const div = document.createElement('div');
let extra = n.inputs.length === 0 ? ' input' : '';
if (n === rootNode) extra = ' output';
div.className = 'node' + extra;
let html = `<div class="op">${n.op === 'input' ? 'input' : n.op}</div>`;
html += `<div class="val">${n.name} = ${fmt(n.val)}</div>`;
if (showGrad) {
html += `<div class="grad">∂L/∂ = ${fmt(n.grad)}</div>`;
}
div.innerHTML = html;
row.appendChild(div);
});
g.appendChild(row);
}
makeRow(inputNodes);
const arr1 = document.createElement('div');
arr1.className = 'row';
arr1.innerHTML = '<span class="arrow">↓ multiply & add</span>';
g.appendChild(arr1);
makeRow(midNodes);
const arr2 = document.createElement('div');
arr2.className = 'row';
arr2.innerHTML = '<span class="arrow">↓ square</span>';
g.appendChild(arr2);
makeRow(outNodes);
if (showGrad) {
const info = document.createElement('p');
info.style = 'font-size:.85rem;color:#0a7d33;margin-top:.5rem;';
info.textContent = '✓ Backward pass complete. Exact gradients computed by the chain rule.';
g.appendChild(info);
}
}
document.getElementById('fwd').addEventListener('click', runForward);
document.getElementById('bwd').addEventListener('click', runBackward);
document.getElementById('reset').addEventListener('click', () => {
nodes = []; rootNode = null; forwardDone = false;
document.getElementById('graph').innerHTML = '';
document.getElementById('bwd').disabled = true;
});
<div class="controls">
<label>x = <input id="xval" type="number" value="2" step="0.5"></label>
<label>w = <input id="wval" type="number" value="3" step="0.5"></label>
<label>b = <input id="bval" type="number" value="-1" step="0.5"></label>
</div>
<div class="btns">
<button id="fwd" type="button">Run forward pass</button>
<button id="bwd" type="button" disabled>Run backward pass</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="graph" class="graph"></div>
<p class="hint">Expression: <b>L = (x·w + b)²</b> — gradients show ∂L/∂node (exact, not approximated).</p>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .6rem; }
.controls label { font-size: .9rem; }
.controls input { width: 64px; padding: .2rem .4rem; border: 1px solid #aaa; border-radius: 6px;
font-size: .9rem; margin-left: .3rem; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
.hint { font-size: .85rem; color: #555; margin-top: .6rem; }
.graph { display: flex; flex-direction: column; gap: 6px; }
.row { display: flex; gap: 6px; flex-wrap: wrap; }
.node { background: #e8eef3; border: 1.5px solid #cdd9e3; border-radius: 10px;
padding: .35rem .6rem; min-width: 90px; text-align: center; font-size: .82rem; line-height: 1.4; }
.node .op { font-weight: 700; font-size: .78rem; color: #1d3557; }
.node .val { font-family: ui-monospace, monospace; font-size: .88rem; }
.node .grad { font-size: .78rem; color: #0a7d33; font-family: ui-monospace, monospace; }
.node.input { background: #dceefb; border-color: #8ab4cc; }
.node.output { background: #d4edda; border-color: #6abf80; }
.node.active { border-color: #e63946; }
.arrow { font-size: 1.1rem; color: #888; align-self: center; }
// Reverse-mode automatic differentiation (backpropagation)
// for the expression L = (x*w + b)^2
//
// We build a computation graph node by node. Each node stores:
// val — forward value
// grad — accumulated gradient ∂L/∂this (set by backward pass)
// backward() — closure that propagates grad to inputs
//
// This is exactly how PyTorch's autograd works at its core.
let nodes = []; // all nodes in creation order
let rootNode = null;
let forwardDone = false;
function makeNode(name, op, inputs) {
const n = { name, op, inputs, val: 0, grad: 0, backward: () => {} };
nodes.push(n);
return n;
}
// Operations: each returns a new node and registers a backward closure
function add(a, b, name) {
const out = makeNode(name, '+', [a, b]);
out.val = a.val + b.val;
out.backward = () => {
a.grad += out.grad; // ∂(a+b)/∂a = 1
b.grad += out.grad; // ∂(a+b)/∂b = 1
};
return out;
}
function mul(a, b, name) {
const out = makeNode(name, '×', [a, b]);
out.val = a.val * b.val;
out.backward = () => {
a.grad += b.val * out.grad; // ∂(a·b)/∂a = b
b.grad += a.val * out.grad; // ∂(a·b)/∂b = a
};
return out;
}
function square(a, name) {
const out = makeNode(name, '²', [a]);
out.val = a.val * a.val;
out.backward = () => {
a.grad += 2 * a.val * out.grad; // ∂(a²)/∂a = 2a
};
return out;
}
function runForward() {
nodes = [];
const xv = parseFloat(document.getElementById('xval').value) || 0;
const wv = parseFloat(document.getElementById('wval').value) || 0;
const bv = parseFloat(document.getElementById('bval').value) || 0;
const x = makeNode('x', 'input', []); x.val = xv;
const w = makeNode('w', 'input', []); w.val = wv;
const b = makeNode('b', 'input', []); b.val = bv;
const xw = mul(x, w, 'x·w');
const s = add(xw, b, 'x·w+b');
const L = square(s, 'L');
rootNode = L;
forwardDone = true;
document.getElementById('bwd').disabled = false;
render(false);
}
function runBackward() {
if (!forwardDone) return;
// Reset grads
nodes.forEach(n => { n.grad = 0; });
rootNode.grad = 1.0; // ∂L/∂L = 1
// Reverse topological order = just reverse creation order for this DAG
for (let i = nodes.length - 1; i >= 0; i--) {
nodes[i].backward();
}
render(true);
}
function fmt(v) {
return (Math.round(v * 10000) / 10000).toString();
}
function render(showGrad) {
const g = document.getElementById('graph');
g.innerHTML = '';
// Group nodes into rows: inputs, intermediate, output
const inputNodes = nodes.filter(n => n.inputs.length === 0);
const midNodes = nodes.filter(n => n.inputs.length > 0 && n !== rootNode);
const outNodes = [rootNode];
function makeRow(group, cls) {
if (!group || !group.length) return;
const row = document.createElement('div');
row.className = 'row';
group.forEach(n => {
const div = document.createElement('div');
let extra = n.inputs.length === 0 ? ' input' : '';
if (n === rootNode) extra = ' output';
div.className = 'node' + extra;
let html = `<div class="op">${n.op === 'input' ? 'input' : n.op}</div>`;
html += `<div class="val">${n.name} = ${fmt(n.val)}</div>`;
if (showGrad) {
html += `<div class="grad">∂L/∂ = ${fmt(n.grad)}</div>`;
}
div.innerHTML = html;
row.appendChild(div);
});
g.appendChild(row);
}
makeRow(inputNodes);
const arr1 = document.createElement('div');
arr1.className = 'row';
arr1.innerHTML = '<span class="arrow">↓ multiply & add</span>';
g.appendChild(arr1);
makeRow(midNodes);
const arr2 = document.createElement('div');
arr2.className = 'row';
arr2.innerHTML = '<span class="arrow">↓ square</span>';
g.appendChild(arr2);
makeRow(outNodes);
if (showGrad) {
const info = document.createElement('p');
info.style = 'font-size:.85rem;color:#0a7d33;margin-top:.5rem;';
info.textContent = '✓ Backward pass complete. Exact gradients computed by the chain rule.';
g.appendChild(info);
}
}
document.getElementById('fwd').addEventListener('click', runForward);
document.getElementById('bwd').addEventListener('click', runBackward);
document.getElementById('reset').addEventListener('click', () => {
nodes = []; rootNode = null; forwardDone = false;
document.getElementById('graph').innerHTML = '';
document.getElementById('bwd').disabled = true;
});
Click Run forward pass to evaluate the expression and build the computation graph. Then click Run backward pass to backpropagate gradients. Each node shows its value and its gradient (∂L/∂node). Notice that the gradient of each input is exact — no approximations, no extra function calls per parameter. Change the values of x, w, and b to see how the gradients shift.
Comments
Loading comments...