The demo below trains a tiny network on a noisy dataset — 60 points drawn from two overlapping classes. The left panel shows training without dropout; the right shows the same architecture trained with 50 % dropout applied to the hidden layer.
<div class="panels">
<div class="panel">
<div class="panel-title">Without Dropout</div>
<canvas id="c0" width="220" height="140"></canvas>
<div class="metrics" id="m0"></div>
</div>
<div class="panel">
<div class="panel-title">With Dropout (50 %)</div>
<canvas id="c1" width="220" height="140"></canvas>
<div class="metrics" id="m1"></div>
</div>
</div>
<div class="legend">
<span class="dot train"></span> Training loss
<span class="dot test"></span> Test loss (dashed)
</div>
<button id="btn" type="button">Reset & retrain</button>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.panels { display: flex; gap: 12px; flex-wrap: wrap; }
.panel { flex: 1 1 200px; }
.panel-title { font-weight: 700; font-size: .85rem; margin-bottom: 4px; text-align: center; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 6px; width: 100%; height: auto; }
.metrics { font-size: .78rem; color: #555; margin-top: 4px; text-align: center; min-height: 2.4em; }
.legend { font-size: .8rem; color: #444; margin: 8px 0 6px; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.dot { display: inline-block; width: 14px; height: 3px; border-radius: 2px; }
.dot.train { background: #1d6fa5; }
.dot.test { background: #e06030; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem 1.1rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; margin-top: 2px; }
button:hover { background: #274b7a; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Mini neural net: 2 inputs -> 8 hidden -> 1 output (sigmoid binary classification).
// Training set: 60 random 2D points, 2 overlapping classes.
// We train two networks in parallel: one with dropout (p=0.5), one without.
// Loss curves (train + test) are drawn on separate canvases.
const SEED0 = 42;
function Rng(s) { let x = s; return () => { x = (x * 1664525 + 1013904223) & 0xffffffff; return (x >>> 0) / 0xffffffff; }; }
function sigmoid(x) { return 1 / (1 + Math.exp(-x)); }
// Generate dataset: 60 points, two overlapping Gaussian blobs
function makeData(rng) {
const X = [], Y = [];
for (let i = 0; i < 60; i++) {
const c = i < 30 ? 0 : 1;
const u1 = rng(), u2 = rng();
const z1 = Math.sqrt(-2 * Math.log(u1 + 1e-9)) * Math.cos(2 * Math.PI * u2);
const z2 = Math.sqrt(-2 * Math.log(u1 + 1e-9)) * Math.sin(2 * Math.PI * u2);
X.push([z1 * 0.9 + (c ? 0.8 : -0.8), z2 * 0.9 + (c ? 0.4 : -0.4)]);
Y.push(c);
}
return { X, Y };
}
// Tiny net: W1 [H x 2], b1 [H], W2 [1 x H], b2 scalar
const H = 8;
function initNet(rng) {
const W1 = [], b1 = [], W2 = [], b2 = [0];
for (let i = 0; i < H; i++) {
W1.push([(rng() - 0.5) * 1.2, (rng() - 0.5) * 1.2]);
b1.push(0);
W2.push((rng() - 0.5) * 1.2);
}
return { W1, b1, W2, b2 };
}
// Forward pass; mask = null means no dropout
function forward(net, x, mask) {
const h = [], ha = [];
for (let i = 0; i < H; i++) {
const z = net.W1[i][0] * x[0] + net.W1[i][1] * x[1] + net.b1[i];
const a = Math.max(0, z); // ReLU
ha.push(a);
h.push(mask ? a * mask[i] : a);
}
let s = net.b2[0];
for (let i = 0; i < H; i++) s += net.W2[i] * h[i];
return { out: sigmoid(s), h, ha };
}
// Binary cross-entropy loss
function loss(net, X, Y, useDrop, rng) {
let total = 0;
for (let n = 0; n < X.length; n++) {
let mask = null;
if (useDrop) { mask = []; for (let i = 0; i < H; i++) mask.push(rng() > 0.5 ? 2 : 0); }
const { out } = forward(net, X[n], mask);
const y = Y[n];
total -= y * Math.log(out + 1e-9) + (1 - y) * Math.log(1 - out + 1e-9);
}
return total / X.length;
}
// SGD with manual backprop
const LR = 0.08;
function trainStep(net, X, Y, useDrop, rng) {
// accumulate gradients
const dW1 = Array.from({ length: H }, () => [0, 0]);
const db1 = new Array(H).fill(0);
const dW2 = new Array(H).fill(0);
let db2 = 0;
for (let n = 0; n < X.length; n++) {
let mask = null;
if (useDrop) { mask = []; for (let i = 0; i < H; i++) mask.push(rng() > 0.5 ? 2 : 0); }
const { out, h, ha } = forward(net, X[n], mask);
const y = Y[n];
const dOut = out - y; // dL/d(logit) = out - y for BCE + sigmoid
db2 += dOut;
for (let i = 0; i < H; i++) {
const hi = mask ? h[i] : ha[i];
dW2[i] += dOut * hi;
const dh = dOut * net.W2[i] * (mask ? mask[i] : 1);
const dz = ha[i] > 0 ? dh : 0; // ReLU grad
db1[i] += dz;
dW1[i][0] += dz * X[n][0];
dW1[i][1] += dz * X[n][1];
}
}
const N = X.length;
net.b2[0] -= LR * db2 / N;
for (let i = 0; i < H; i++) {
net.W2[i] -= LR * dW2[i] / N;
net.b1[i] -= LR * db1[i] / N;
net.W1[i][0] -= LR * dW1[i][0] / N;
net.W1[i][1] -= LR * dW1[i][1] / N;
}
}
// Draw loss curve on canvas
function drawCurves(canvasId, trainHist, testHist, epoch) {
const cv = document.getElementById(canvasId);
const ctx = cv.getContext('2d');
const W = cv.width, H2 = cv.height, pad = 28;
ctx.clearRect(0, 0, W, H2);
ctx.fillStyle = '#f7f9fb'; ctx.fillRect(0, 0, W, H2);
// axes
ctx.strokeStyle = '#cdd9e3'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(pad, 8); ctx.lineTo(pad, H2 - pad);
ctx.lineTo(W - 8, H2 - pad); ctx.stroke();
const maxL = Math.max(2, ...trainHist, ...testHist);
const scaleX = (ep) => pad + (ep / 200) * (W - pad - 8);
const scaleY = (v) => H2 - pad - (v / maxL) * (H2 - pad - 8);
function drawLine(hist, color, dashed) {
if (hist.length < 2) return;
ctx.strokeStyle = color; ctx.lineWidth = 2;
ctx.setLineDash(dashed ? [5, 4] : []);
ctx.beginPath();
hist.forEach((v, i) => {
const x = scaleX(i), y = scaleY(v);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
ctx.setLineDash([]);
}
drawLine(trainHist, '#1d6fa5', false);
drawLine(testHist, '#e06030', true);
// y-axis label
ctx.fillStyle = '#888'; ctx.font = '9px system-ui'; ctx.textAlign = 'right';
ctx.fillText(maxL.toFixed(1), pad - 3, 12);
ctx.fillText('0', pad - 3, H2 - pad + 4);
ctx.textAlign = 'center';
ctx.fillText('Epoch', W / 2, H2 - 4);
}
// State
let state = null;
function reset() {
const rng = Rng(SEED0 + Math.floor(Math.random() * 9999));
const data = makeData(rng);
const n = data.X.length;
const idx = Array.from({ length: n }, (_, i) => i).sort(() => rng() - 0.5);
const split = Math.floor(n * 0.7);
const trainIdx = idx.slice(0, split), testIdx = idx.slice(split);
const trainX = trainIdx.map(i => data.X[i]), trainY = trainIdx.map(i => data.Y[i]);
const testX = testIdx.map(i => data.X[i]), testY = testIdx.map(i => data.Y[i]);
const rng0 = Rng(7), rng1 = Rng(7); // same init weights
state = {
net0: initNet(rng0), net1: initNet(rng1),
trainX, trainY, testX, testY,
hist0: { train: [], test: [] },
hist1: { train: [], test: [] },
epoch: 0,
rng: Rng(SEED0 + 1)
};
}
function step() {
if (!state) return;
const { net0, net1, trainX, trainY, testX, testY, rng } = state;
const rng0 = Rng(state.epoch * 31 + 7);
const rng1 = Rng(state.epoch * 31 + 13);
trainStep(net0, trainX, trainY, false, rng0);
trainStep(net1, trainX, trainY, true, rng1);
if (state.epoch % 5 === 0) {
const tl0 = loss(net0, trainX, trainY, false, rng0);
const te0 = loss(net0, testX, testY, false, rng0);
const tl1 = loss(net1, trainX, trainY, false, rng1); // eval without dropout
const te1 = loss(net1, testX, testY, false, rng1);
state.hist0.train.push(tl0); state.hist0.test.push(te0);
state.hist1.train.push(tl1); state.hist1.test.push(te1);
drawCurves('c0', state.hist0.train, state.hist0.test, state.epoch);
drawCurves('c1', state.hist1.train, state.hist1.test, state.epoch);
const fmt = v => v.toFixed(3);
document.getElementById('m0').textContent =
i18nFmt('Epoch %epoch% | Train: %fmt% | Test: %fmt2% | Gap: %fmt3%', { epoch: state.epoch, fmt: fmt(tl0), fmt2: fmt(te0), fmt3: fmt(te0-tl0) });
document.getElementById('m1').textContent =
i18nFmt('Epoch %epoch% | Train: %fmt% | Test: %fmt2% | Gap: %fmt3%', { epoch: state.epoch, fmt: fmt(tl1), fmt2: fmt(te1), fmt3: fmt(te1-tl1) });
}
state.epoch++;
}
let timer = null;
function startTraining() {
if (timer) clearInterval(timer);
reset();
timer = setInterval(() => { for (let i = 0; i < 5; i++) step(); if (state.epoch >= 200) clearInterval(timer); }, 30);
}
document.getElementById('btn').addEventListener('click', startTraining);
startTraining();
<div class="panels">
<div class="panel">
<div class="panel-title">Without Dropout</div>
<canvas id="c0" width="220" height="140"></canvas>
<div class="metrics" id="m0"></div>
</div>
<div class="panel">
<div class="panel-title">With Dropout (50 %)</div>
<canvas id="c1" width="220" height="140"></canvas>
<div class="metrics" id="m1"></div>
</div>
</div>
<div class="legend">
<span class="dot train"></span> Training loss
<span class="dot test"></span> Test loss (dashed)
</div>
<button id="btn" type="button">Reset & retrain</button>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.panels { display: flex; gap: 12px; flex-wrap: wrap; }
.panel { flex: 1 1 200px; }
.panel-title { font-weight: 700; font-size: .85rem; margin-bottom: 4px; text-align: center; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 6px; width: 100%; height: auto; }
.metrics { font-size: .78rem; color: #555; margin-top: 4px; text-align: center; min-height: 2.4em; }
.legend { font-size: .8rem; color: #444; margin: 8px 0 6px; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.dot { display: inline-block; width: 14px; height: 3px; border-radius: 2px; }
.dot.train { background: #1d6fa5; }
.dot.test { background: #e06030; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem 1.1rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; margin-top: 2px; }
button:hover { background: #274b7a; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Mini neural net: 2 inputs -> 8 hidden -> 1 output (sigmoid binary classification).
// Training set: 60 random 2D points, 2 overlapping classes.
// We train two networks in parallel: one with dropout (p=0.5), one without.
// Loss curves (train + test) are drawn on separate canvases.
const SEED0 = 42;
function Rng(s) { let x = s; return () => { x = (x * 1664525 + 1013904223) & 0xffffffff; return (x >>> 0) / 0xffffffff; }; }
function sigmoid(x) { return 1 / (1 + Math.exp(-x)); }
// Generate dataset: 60 points, two overlapping Gaussian blobs
function makeData(rng) {
const X = [], Y = [];
for (let i = 0; i < 60; i++) {
const c = i < 30 ? 0 : 1;
const u1 = rng(), u2 = rng();
const z1 = Math.sqrt(-2 * Math.log(u1 + 1e-9)) * Math.cos(2 * Math.PI * u2);
const z2 = Math.sqrt(-2 * Math.log(u1 + 1e-9)) * Math.sin(2 * Math.PI * u2);
X.push([z1 * 0.9 + (c ? 0.8 : -0.8), z2 * 0.9 + (c ? 0.4 : -0.4)]);
Y.push(c);
}
return { X, Y };
}
// Tiny net: W1 [H x 2], b1 [H], W2 [1 x H], b2 scalar
const H = 8;
function initNet(rng) {
const W1 = [], b1 = [], W2 = [], b2 = [0];
for (let i = 0; i < H; i++) {
W1.push([(rng() - 0.5) * 1.2, (rng() - 0.5) * 1.2]);
b1.push(0);
W2.push((rng() - 0.5) * 1.2);
}
return { W1, b1, W2, b2 };
}
// Forward pass; mask = null means no dropout
function forward(net, x, mask) {
const h = [], ha = [];
for (let i = 0; i < H; i++) {
const z = net.W1[i][0] * x[0] + net.W1[i][1] * x[1] + net.b1[i];
const a = Math.max(0, z); // ReLU
ha.push(a);
h.push(mask ? a * mask[i] : a);
}
let s = net.b2[0];
for (let i = 0; i < H; i++) s += net.W2[i] * h[i];
return { out: sigmoid(s), h, ha };
}
// Binary cross-entropy loss
function loss(net, X, Y, useDrop, rng) {
let total = 0;
for (let n = 0; n < X.length; n++) {
let mask = null;
if (useDrop) { mask = []; for (let i = 0; i < H; i++) mask.push(rng() > 0.5 ? 2 : 0); }
const { out } = forward(net, X[n], mask);
const y = Y[n];
total -= y * Math.log(out + 1e-9) + (1 - y) * Math.log(1 - out + 1e-9);
}
return total / X.length;
}
// SGD with manual backprop
const LR = 0.08;
function trainStep(net, X, Y, useDrop, rng) {
// accumulate gradients
const dW1 = Array.from({ length: H }, () => [0, 0]);
const db1 = new Array(H).fill(0);
const dW2 = new Array(H).fill(0);
let db2 = 0;
for (let n = 0; n < X.length; n++) {
let mask = null;
if (useDrop) { mask = []; for (let i = 0; i < H; i++) mask.push(rng() > 0.5 ? 2 : 0); }
const { out, h, ha } = forward(net, X[n], mask);
const y = Y[n];
const dOut = out - y; // dL/d(logit) = out - y for BCE + sigmoid
db2 += dOut;
for (let i = 0; i < H; i++) {
const hi = mask ? h[i] : ha[i];
dW2[i] += dOut * hi;
const dh = dOut * net.W2[i] * (mask ? mask[i] : 1);
const dz = ha[i] > 0 ? dh : 0; // ReLU grad
db1[i] += dz;
dW1[i][0] += dz * X[n][0];
dW1[i][1] += dz * X[n][1];
}
}
const N = X.length;
net.b2[0] -= LR * db2 / N;
for (let i = 0; i < H; i++) {
net.W2[i] -= LR * dW2[i] / N;
net.b1[i] -= LR * db1[i] / N;
net.W1[i][0] -= LR * dW1[i][0] / N;
net.W1[i][1] -= LR * dW1[i][1] / N;
}
}
// Draw loss curve on canvas
function drawCurves(canvasId, trainHist, testHist, epoch) {
const cv = document.getElementById(canvasId);
const ctx = cv.getContext('2d');
const W = cv.width, H2 = cv.height, pad = 28;
ctx.clearRect(0, 0, W, H2);
ctx.fillStyle = '#f7f9fb'; ctx.fillRect(0, 0, W, H2);
// axes
ctx.strokeStyle = '#cdd9e3'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(pad, 8); ctx.lineTo(pad, H2 - pad);
ctx.lineTo(W - 8, H2 - pad); ctx.stroke();
const maxL = Math.max(2, ...trainHist, ...testHist);
const scaleX = (ep) => pad + (ep / 200) * (W - pad - 8);
const scaleY = (v) => H2 - pad - (v / maxL) * (H2 - pad - 8);
function drawLine(hist, color, dashed) {
if (hist.length < 2) return;
ctx.strokeStyle = color; ctx.lineWidth = 2;
ctx.setLineDash(dashed ? [5, 4] : []);
ctx.beginPath();
hist.forEach((v, i) => {
const x = scaleX(i), y = scaleY(v);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
ctx.setLineDash([]);
}
drawLine(trainHist, '#1d6fa5', false);
drawLine(testHist, '#e06030', true);
// y-axis label
ctx.fillStyle = '#888'; ctx.font = '9px system-ui'; ctx.textAlign = 'right';
ctx.fillText(maxL.toFixed(1), pad - 3, 12);
ctx.fillText('0', pad - 3, H2 - pad + 4);
ctx.textAlign = 'center';
ctx.fillText('Epoch', W / 2, H2 - 4);
}
// State
let state = null;
function reset() {
const rng = Rng(SEED0 + Math.floor(Math.random() * 9999));
const data = makeData(rng);
const n = data.X.length;
const idx = Array.from({ length: n }, (_, i) => i).sort(() => rng() - 0.5);
const split = Math.floor(n * 0.7);
const trainIdx = idx.slice(0, split), testIdx = idx.slice(split);
const trainX = trainIdx.map(i => data.X[i]), trainY = trainIdx.map(i => data.Y[i]);
const testX = testIdx.map(i => data.X[i]), testY = testIdx.map(i => data.Y[i]);
const rng0 = Rng(7), rng1 = Rng(7); // same init weights
state = {
net0: initNet(rng0), net1: initNet(rng1),
trainX, trainY, testX, testY,
hist0: { train: [], test: [] },
hist1: { train: [], test: [] },
epoch: 0,
rng: Rng(SEED0 + 1)
};
}
function step() {
if (!state) return;
const { net0, net1, trainX, trainY, testX, testY, rng } = state;
const rng0 = Rng(state.epoch * 31 + 7);
const rng1 = Rng(state.epoch * 31 + 13);
trainStep(net0, trainX, trainY, false, rng0);
trainStep(net1, trainX, trainY, true, rng1);
if (state.epoch % 5 === 0) {
const tl0 = loss(net0, trainX, trainY, false, rng0);
const te0 = loss(net0, testX, testY, false, rng0);
const tl1 = loss(net1, trainX, trainY, false, rng1); // eval without dropout
const te1 = loss(net1, testX, testY, false, rng1);
state.hist0.train.push(tl0); state.hist0.test.push(te0);
state.hist1.train.push(tl1); state.hist1.test.push(te1);
drawCurves('c0', state.hist0.train, state.hist0.test, state.epoch);
drawCurves('c1', state.hist1.train, state.hist1.test, state.epoch);
const fmt = v => v.toFixed(3);
document.getElementById('m0').textContent =
i18nFmt('Epoch %epoch% | Train: %fmt% | Test: %fmt2% | Gap: %fmt3%', { epoch: state.epoch, fmt: fmt(tl0), fmt2: fmt(te0), fmt3: fmt(te0-tl0) });
document.getElementById('m1').textContent =
i18nFmt('Epoch %epoch% | Train: %fmt% | Test: %fmt2% | Gap: %fmt3%', { epoch: state.epoch, fmt: fmt(tl1), fmt2: fmt(te1), fmt3: fmt(te1-tl1) });
}
state.epoch++;
}
let timer = null;
function startTraining() {
if (timer) clearInterval(timer);
reset();
timer = setInterval(() => { for (let i = 0; i < 5; i++) step(); if (state.epoch >= 200) clearInterval(timer); }, 30);
}
document.getElementById('btn').addEventListener('click', startTraining);
startTraining();
Comments
Loading comments...