Below is a tiny network learning to classify two groups of points. It has more weights than it needs. Follow the lottery ticket recipe:
<p class="hint">A tiny 2-layer network learns to separate two point clouds. Train it, then prune the smallest weights and retrain only the survivors — reset to their lucky initial values.</p>
<canvas id="canvas" width="260" height="200"></canvas>
<div class="controls">
<label>Prune ratio: <span id="prune-val">50%</span>
<input type="range" id="prune-slider" min="0" max="90" step="10" value="50">
</label>
</div>
<div class="btns">
<button id="btn-train" type="button">1. Train full network</button>
<button id="btn-prune" type="button" disabled>2. Prune & retrain ticket</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
<div id="stats" class="stats"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; display: block; background: #f7f9fb; }
.controls { margin: .5rem 0; font-size: .9rem; }
.controls label { display: flex; align-items: center; gap: .5rem; }
input[type=range] { flex: 1; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
button { font: 600 13px system-ui, sans-serif; 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; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status.ok { color: #0a7d33; }
.status.run { color: #1d3557; }
.stats { font-size: .82rem; color: #555; min-height: 1.2em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Lottery Ticket Hypothesis demo
// Network: 2 inputs -> 6 hidden (ReLU) -> 1 output (sigmoid)
// Dataset: two linearly separable point clouds
// After training, prune smallest weights by magnitude, reset survivors to their initial values, retrain — the winning ticket recovers accuracy.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// --- Dataset ---
function makeData(n, seed) {
let s = seed;
function rand() { s = (s * 1664525 + 1013904223) & 0xffffffff; return (s >>> 0) / 4294967296; }
const pts = [];
for (let i = 0; i < n; i++) {
const label = i < n / 2 ? 0 : 1;
const cx = label === 0 ? -1.2 : 1.2;
const cy = label === 0 ? -0.8 : 0.8;
pts.push({ x: cx + (rand() - 0.5) * 1.4, y: cy + (rand() - 0.5) * 1.4, label });
}
return pts;
}
const DATA = makeData(60, 42);
// --- Network (2->6->1) ---
const N_IN = 2, N_H = 6, N_OUT = 1;
function randn(seed) {
// Box-Muller via LCG
let s = seed;
return function() {
s = (s * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn;
const u1 = Number((s >> 33n) & 0xfffffffn) / 268435455;
s = (s * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn;
const u2 = Number((s >> 33n) & 0xfffffffn) / 268435455;
return Math.sqrt(-2 * Math.log(u1 + 1e-9)) * Math.cos(2 * Math.PI * u2);
};
}
function initWeights(seedVal) {
const rng = randn(BigInt(seedVal));
const scale1 = Math.sqrt(2 / N_IN);
const scale2 = Math.sqrt(2 / N_H);
// W1: N_H x N_IN, b1: N_H, W2: N_OUT x N_H, b2: N_OUT
const W1 = Array.from({length: N_H}, () => Array.from({length: N_IN}, () => rng() * scale1));
const b1 = Array(N_H).fill(0);
const W2 = Array.from({length: N_OUT}, () => Array.from({length: N_H}, () => rng() * scale2));
const b2 = Array(N_OUT).fill(0);
return { W1, b1, W2, b2 };
}
function cloneWeights(w) {
return {
W1: w.W1.map(r => [...r]),
b1: [...w.b1],
W2: w.W2.map(r => [...r]),
b2: [...w.b2]
};
}
function sigmoid(x) { return 1 / (1 + Math.exp(-x)); }
function relu(x) { return x > 0 ? x : 0; }
function forward(net, x0, x1) {
const h = Array(N_H);
for (let j = 0; j < N_H; j++)
h[j] = relu(net.W1[j][0] * x0 + net.W1[j][1] * x1 + net.b1[j]);
let out = net.b2[0];
for (let j = 0; j < N_H; j++) out += net.W2[0][j] * h[j];
return sigmoid(out);
}
function accuracy(net) {
let correct = 0;
for (const p of DATA) {
const pred = forward(net, p.x, p.y) > 0.5 ? 1 : 0;
if (pred === p.label) correct++;
}
return correct / DATA.length;
}
// SGD with backprop
function trainStep(net, lr, mask) {
// mask: same shape as weights, 0=frozen, 1=active
const batchSize = DATA.length;
const dW1 = net.W1.map(r => r.map(() => 0));
const db1 = Array(N_H).fill(0);
const dW2 = net.W2.map(r => r.map(() => 0));
const db2 = Array(N_OUT).fill(0);
let loss = 0;
for (const p of DATA) {
const x0 = p.x, x1 = p.y, y = p.label;
// forward
const pre = Array(N_H);
const h = Array(N_H);
for (let j = 0; j < N_H; j++) {
pre[j] = net.W1[j][0] * x0 + net.W1[j][1] * x1 + net.b1[j];
h[j] = relu(pre[j]);
}
let outPre = net.b2[0];
for (let j = 0; j < N_H; j++) outPre += net.W2[0][j] * h[j];
const out = sigmoid(outPre);
loss += -(y * Math.log(out + 1e-9) + (1 - y) * Math.log(1 - out + 1e-9));
// backward
const dOut = out - y;
db2[0] += dOut;
for (let j = 0; j < N_H; j++) {
dW2[0][j] += dOut * h[j];
const dh = dOut * net.W2[0][j];
const dpre = dh * (pre[j] > 0 ? 1 : 0);
db1[j] += dpre;
dW1[j][0] += dpre * x0;
dW1[j][1] += dpre * x1;
}
}
// apply gradients (with mask)
for (let j = 0; j < N_H; j++) {
for (let k = 0; k < N_IN; k++)
net.W1[j][k] -= lr / batchSize * dW1[j][k] * (mask ? mask.W1[j][k] : 1);
net.b1[j] -= lr / batchSize * db1[j] * (mask ? mask.b1[j] : 1);
}
for (let j = 0; j < N_H; j++)
net.W2[0][j] -= lr / batchSize * dW2[0][j] * (mask ? mask.W2[0][j] : 1);
net.b2[0] -= lr / batchSize * db2[0];
return loss / batchSize;
}
// --- State ---
let initNet = null; // original random weights
let trainedNet = null; // fully trained network
let ticketNet = null; // retrained winning ticket
let pruneMask = null; // 1=keep, 0=pruned
function getState() {
if (ticketNet) return 'ticket';
if (trainedNet) return 'trained';
return 'init';
}
// --- Pruning ---
function buildMask(net, ratio) {
// Collect all (weight, ref) pairs (biases excluded from pruning for clarity)
const entries = [];
for (let j = 0; j < N_H; j++)
for (let k = 0; k < N_IN; k++)
entries.push({ abs: Math.abs(net.W1[j][k]), type: 'W1', j, k });
for (let j = 0; j < N_H; j++)
entries.push({ abs: Math.abs(net.W2[0][j]), type: 'W2', j });
entries.sort((a, b) => a.abs - b.abs);
const nPrune = Math.floor(entries.length * ratio);
const pruned = new Set(entries.slice(0, nPrune).map((_, i) => i));
const mask = {
W1: net.W1.map(r => r.map(() => 1)),
b1: Array(N_H).fill(1),
W2: net.W2.map(r => r.map(() => 1)),
b2: Array(N_OUT).fill(1)
};
entries.slice(0, nPrune).forEach(e => {
if (e.type === 'W1') mask.W1[e.j][e.k] = 0;
else mask.W2[0][e.j] = 0;
});
return { mask, nPrune, total: entries.length };
}
// --- Drawing ---
function toCanvas(v, lo, hi, dim) {
return ((v - lo) / (hi - lo)) * dim;
}
const XLO = -3, XHI = 3, YLO = -2.5, YHI = 2.5;
function drawBackground(net) {
const imgData = ctx.createImageData(W, H);
for (let px = 0; px < W; px++) {
for (let py = 0; py < H; py++) {
const wx = XLO + (px / W) * (XHI - XLO);
const wy = YHI - (py / H) * (YHI - YLO);
const p = forward(net, wx, wy);
const i = (py * W + px) * 4;
if (p > 0.5) {
imgData.data[i] = 220; imgData.data[i+1] = 235; imgData.data[i+2] = 255;
} else {
imgData.data[i] = 255; imgData.data[i+1] = 230; imgData.data[i+2] = 220;
}
imgData.data[i+3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
}
function drawPoints(net) {
for (const p of DATA) {
const cx = toCanvas(p.x, XLO, XHI, W);
const cy = H - toCanvas(p.y, YLO, YHI, H);
ctx.beginPath();
ctx.arc(cx, cy, 4.5, 0, Math.PI * 2);
ctx.fillStyle = p.label === 1 ? '#1d6fbf' : '#c92f3c';
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.2;
ctx.fill();
ctx.stroke();
}
}
function render(net) {
ctx.clearRect(0, 0, W, H);
drawBackground(net);
drawPoints(net);
}
// Initial dummy net
function zeroNet() {
return {
W1: Array.from({length: N_H}, () => Array(N_IN).fill(0)),
b1: Array(N_H).fill(0),
W2: Array.from({length: N_OUT}, () => Array(N_H).fill(0)),
b2: Array(N_OUT).fill(0)
};
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + (cls || '');
}
function setStats(msg) { document.getElementById('stats').textContent = msg; }
// --- Button handlers ---
const btnTrain = document.getElementById('btn-train');
const btnPrune = document.getElementById('btn-prune');
const btnReset = document.getElementById('btn-reset');
function doReset() {
initNet = null; trainedNet = null; ticketNet = null; pruneMask = null;
render(zeroNet());
setStatus('');
setStats('');
btnTrain.disabled = false;
btnPrune.disabled = true;
}
function doTrain() {
btnTrain.disabled = true;
btnPrune.disabled = true;
setStatus('Training full network…', 'run');
initNet = initWeights(7);
const net = cloneWeights(initNet);
const EPOCHS = 300;
let epoch = 0;
function step() {
for (let i = 0; i < 20; i++) trainStep(net, 0.8, null);
epoch += 20;
render(net);
if (epoch < EPOCHS) {
requestAnimationFrame(step);
} else {
trainedNet = cloneWeights(net);
const acc = (accuracy(trainedNet) * 100).toFixed(1);
setStatus(fmt('Full network trained. Accuracy: %acc%%', { acc: acc }), 'ok');
setStats(fmt('All %v%%N_H% weights active.', { v: N_H * N_IN, N_H: N_H }));
btnPrune.disabled = false;
}
}
requestAnimationFrame(step);
}
function doPrune() {
if (!trainedNet) return;
btnPrune.disabled = true;
btnTrain.disabled = true;
const ratio = parseInt(document.getElementById('prune-slider').value) / 100;
const { mask, nPrune, total } = buildMask(trainedNet, ratio);
pruneMask = mask;
// Build ticket: initial weights but zeroed where pruned
const ticket = cloneWeights(initNet);
for (let j = 0; j < N_H; j++)
for (let k = 0; k < N_IN; k++)
if (!mask.W1[j][k]) ticket.W1[j][k] = 0;
for (let j = 0; j < N_H; j++)
if (!mask.W2[0][j]) ticket.W2[0][j] = 0;
setStatus(fmt('Retraining winning ticket (%v%/%total% weights)…', { v: total - nPrune, total: total }), 'run');
const EPOCHS = 300;
let epoch = 0;
function step() {
for (let i = 0; i < 20; i++) trainStep(ticket, 0.8, mask);
epoch += 20;
render(ticket);
if (epoch < EPOCHS) {
requestAnimationFrame(step);
} else {
ticketNet = cloneWeights(ticket);
const accFull = (accuracy(trainedNet) * 100).toFixed(1);
const accTicket = (accuracy(ticketNet) * 100).toFixed(1);
const kept = total - nPrune;
setStatus(
fmt('Ticket accuracy: %accTicket%% vs full: %accFull%%%v%', { accTicket: accTicket, accFull: accFull, v: ' — ' + (parseFloat(accTicket) >= parseFloat(accFull) - 5 ? 'winning ticket! 🎫' : 'ticket lost this time.') }),
parseFloat(accTicket) >= parseFloat(accFull) - 5 ? 'ok' : ''
);
setStats(fmt('%kept%/%total% weights kept (%round%% pruned). Size reduction: %round2%%.', { kept: kept, total: total, round: Math.round(ratio * 100), round2: Math.round((1 - kept / total) * 100) }));
btnPrune.disabled = false;
}
}
requestAnimationFrame(step);
}
btnTrain.addEventListener('click', doTrain);
btnPrune.addEventListener('click', doPrune);
btnReset.addEventListener('click', doReset);
document.getElementById('prune-slider').addEventListener('input', function() {
document.getElementById('prune-val').textContent = this.value + '%';
});
// Initial render
doReset();
render(zeroNet());
<p class="hint">A tiny 2-layer network learns to separate two point clouds. Train it, then prune the smallest weights and retrain only the survivors — reset to their lucky initial values.</p>
<canvas id="canvas" width="260" height="200"></canvas>
<div class="controls">
<label>Prune ratio: <span id="prune-val">50%</span>
<input type="range" id="prune-slider" min="0" max="90" step="10" value="50">
</label>
</div>
<div class="btns">
<button id="btn-train" type="button">1. Train full network</button>
<button id="btn-prune" type="button" disabled>2. Prune & retrain ticket</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="status" class="status"></div>
<div id="stats" class="stats"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; display: block; background: #f7f9fb; }
.controls { margin: .5rem 0; font-size: .9rem; }
.controls label { display: flex; align-items: center; gap: .5rem; }
input[type=range] { flex: 1; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
button { font: 600 13px system-ui, sans-serif; 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; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status.ok { color: #0a7d33; }
.status.run { color: #1d3557; }
.stats { font-size: .82rem; color: #555; min-height: 1.2em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Lottery Ticket Hypothesis demo
// Network: 2 inputs -> 6 hidden (ReLU) -> 1 output (sigmoid)
// Dataset: two linearly separable point clouds
// After training, prune smallest weights by magnitude, reset survivors to their initial values, retrain — the winning ticket recovers accuracy.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// --- Dataset ---
function makeData(n, seed) {
let s = seed;
function rand() { s = (s * 1664525 + 1013904223) & 0xffffffff; return (s >>> 0) / 4294967296; }
const pts = [];
for (let i = 0; i < n; i++) {
const label = i < n / 2 ? 0 : 1;
const cx = label === 0 ? -1.2 : 1.2;
const cy = label === 0 ? -0.8 : 0.8;
pts.push({ x: cx + (rand() - 0.5) * 1.4, y: cy + (rand() - 0.5) * 1.4, label });
}
return pts;
}
const DATA = makeData(60, 42);
// --- Network (2->6->1) ---
const N_IN = 2, N_H = 6, N_OUT = 1;
function randn(seed) {
// Box-Muller via LCG
let s = seed;
return function() {
s = (s * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn;
const u1 = Number((s >> 33n) & 0xfffffffn) / 268435455;
s = (s * 6364136223846793005n + 1442695040888963407n) & 0xffffffffffffffffn;
const u2 = Number((s >> 33n) & 0xfffffffn) / 268435455;
return Math.sqrt(-2 * Math.log(u1 + 1e-9)) * Math.cos(2 * Math.PI * u2);
};
}
function initWeights(seedVal) {
const rng = randn(BigInt(seedVal));
const scale1 = Math.sqrt(2 / N_IN);
const scale2 = Math.sqrt(2 / N_H);
// W1: N_H x N_IN, b1: N_H, W2: N_OUT x N_H, b2: N_OUT
const W1 = Array.from({length: N_H}, () => Array.from({length: N_IN}, () => rng() * scale1));
const b1 = Array(N_H).fill(0);
const W2 = Array.from({length: N_OUT}, () => Array.from({length: N_H}, () => rng() * scale2));
const b2 = Array(N_OUT).fill(0);
return { W1, b1, W2, b2 };
}
function cloneWeights(w) {
return {
W1: w.W1.map(r => [...r]),
b1: [...w.b1],
W2: w.W2.map(r => [...r]),
b2: [...w.b2]
};
}
function sigmoid(x) { return 1 / (1 + Math.exp(-x)); }
function relu(x) { return x > 0 ? x : 0; }
function forward(net, x0, x1) {
const h = Array(N_H);
for (let j = 0; j < N_H; j++)
h[j] = relu(net.W1[j][0] * x0 + net.W1[j][1] * x1 + net.b1[j]);
let out = net.b2[0];
for (let j = 0; j < N_H; j++) out += net.W2[0][j] * h[j];
return sigmoid(out);
}
function accuracy(net) {
let correct = 0;
for (const p of DATA) {
const pred = forward(net, p.x, p.y) > 0.5 ? 1 : 0;
if (pred === p.label) correct++;
}
return correct / DATA.length;
}
// SGD with backprop
function trainStep(net, lr, mask) {
// mask: same shape as weights, 0=frozen, 1=active
const batchSize = DATA.length;
const dW1 = net.W1.map(r => r.map(() => 0));
const db1 = Array(N_H).fill(0);
const dW2 = net.W2.map(r => r.map(() => 0));
const db2 = Array(N_OUT).fill(0);
let loss = 0;
for (const p of DATA) {
const x0 = p.x, x1 = p.y, y = p.label;
// forward
const pre = Array(N_H);
const h = Array(N_H);
for (let j = 0; j < N_H; j++) {
pre[j] = net.W1[j][0] * x0 + net.W1[j][1] * x1 + net.b1[j];
h[j] = relu(pre[j]);
}
let outPre = net.b2[0];
for (let j = 0; j < N_H; j++) outPre += net.W2[0][j] * h[j];
const out = sigmoid(outPre);
loss += -(y * Math.log(out + 1e-9) + (1 - y) * Math.log(1 - out + 1e-9));
// backward
const dOut = out - y;
db2[0] += dOut;
for (let j = 0; j < N_H; j++) {
dW2[0][j] += dOut * h[j];
const dh = dOut * net.W2[0][j];
const dpre = dh * (pre[j] > 0 ? 1 : 0);
db1[j] += dpre;
dW1[j][0] += dpre * x0;
dW1[j][1] += dpre * x1;
}
}
// apply gradients (with mask)
for (let j = 0; j < N_H; j++) {
for (let k = 0; k < N_IN; k++)
net.W1[j][k] -= lr / batchSize * dW1[j][k] * (mask ? mask.W1[j][k] : 1);
net.b1[j] -= lr / batchSize * db1[j] * (mask ? mask.b1[j] : 1);
}
for (let j = 0; j < N_H; j++)
net.W2[0][j] -= lr / batchSize * dW2[0][j] * (mask ? mask.W2[0][j] : 1);
net.b2[0] -= lr / batchSize * db2[0];
return loss / batchSize;
}
// --- State ---
let initNet = null; // original random weights
let trainedNet = null; // fully trained network
let ticketNet = null; // retrained winning ticket
let pruneMask = null; // 1=keep, 0=pruned
function getState() {
if (ticketNet) return 'ticket';
if (trainedNet) return 'trained';
return 'init';
}
// --- Pruning ---
function buildMask(net, ratio) {
// Collect all (weight, ref) pairs (biases excluded from pruning for clarity)
const entries = [];
for (let j = 0; j < N_H; j++)
for (let k = 0; k < N_IN; k++)
entries.push({ abs: Math.abs(net.W1[j][k]), type: 'W1', j, k });
for (let j = 0; j < N_H; j++)
entries.push({ abs: Math.abs(net.W2[0][j]), type: 'W2', j });
entries.sort((a, b) => a.abs - b.abs);
const nPrune = Math.floor(entries.length * ratio);
const pruned = new Set(entries.slice(0, nPrune).map((_, i) => i));
const mask = {
W1: net.W1.map(r => r.map(() => 1)),
b1: Array(N_H).fill(1),
W2: net.W2.map(r => r.map(() => 1)),
b2: Array(N_OUT).fill(1)
};
entries.slice(0, nPrune).forEach(e => {
if (e.type === 'W1') mask.W1[e.j][e.k] = 0;
else mask.W2[0][e.j] = 0;
});
return { mask, nPrune, total: entries.length };
}
// --- Drawing ---
function toCanvas(v, lo, hi, dim) {
return ((v - lo) / (hi - lo)) * dim;
}
const XLO = -3, XHI = 3, YLO = -2.5, YHI = 2.5;
function drawBackground(net) {
const imgData = ctx.createImageData(W, H);
for (let px = 0; px < W; px++) {
for (let py = 0; py < H; py++) {
const wx = XLO + (px / W) * (XHI - XLO);
const wy = YHI - (py / H) * (YHI - YLO);
const p = forward(net, wx, wy);
const i = (py * W + px) * 4;
if (p > 0.5) {
imgData.data[i] = 220; imgData.data[i+1] = 235; imgData.data[i+2] = 255;
} else {
imgData.data[i] = 255; imgData.data[i+1] = 230; imgData.data[i+2] = 220;
}
imgData.data[i+3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
}
function drawPoints(net) {
for (const p of DATA) {
const cx = toCanvas(p.x, XLO, XHI, W);
const cy = H - toCanvas(p.y, YLO, YHI, H);
ctx.beginPath();
ctx.arc(cx, cy, 4.5, 0, Math.PI * 2);
ctx.fillStyle = p.label === 1 ? '#1d6fbf' : '#c92f3c';
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.2;
ctx.fill();
ctx.stroke();
}
}
function render(net) {
ctx.clearRect(0, 0, W, H);
drawBackground(net);
drawPoints(net);
}
// Initial dummy net
function zeroNet() {
return {
W1: Array.from({length: N_H}, () => Array(N_IN).fill(0)),
b1: Array(N_H).fill(0),
W2: Array.from({length: N_OUT}, () => Array(N_H).fill(0)),
b2: Array(N_OUT).fill(0)
};
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + (cls || '');
}
function setStats(msg) { document.getElementById('stats').textContent = msg; }
// --- Button handlers ---
const btnTrain = document.getElementById('btn-train');
const btnPrune = document.getElementById('btn-prune');
const btnReset = document.getElementById('btn-reset');
function doReset() {
initNet = null; trainedNet = null; ticketNet = null; pruneMask = null;
render(zeroNet());
setStatus('');
setStats('');
btnTrain.disabled = false;
btnPrune.disabled = true;
}
function doTrain() {
btnTrain.disabled = true;
btnPrune.disabled = true;
setStatus('Training full network…', 'run');
initNet = initWeights(7);
const net = cloneWeights(initNet);
const EPOCHS = 300;
let epoch = 0;
function step() {
for (let i = 0; i < 20; i++) trainStep(net, 0.8, null);
epoch += 20;
render(net);
if (epoch < EPOCHS) {
requestAnimationFrame(step);
} else {
trainedNet = cloneWeights(net);
const acc = (accuracy(trainedNet) * 100).toFixed(1);
setStatus(fmt('Full network trained. Accuracy: %acc%%', { acc: acc }), 'ok');
setStats(fmt('All %v%%N_H% weights active.', { v: N_H * N_IN, N_H: N_H }));
btnPrune.disabled = false;
}
}
requestAnimationFrame(step);
}
function doPrune() {
if (!trainedNet) return;
btnPrune.disabled = true;
btnTrain.disabled = true;
const ratio = parseInt(document.getElementById('prune-slider').value) / 100;
const { mask, nPrune, total } = buildMask(trainedNet, ratio);
pruneMask = mask;
// Build ticket: initial weights but zeroed where pruned
const ticket = cloneWeights(initNet);
for (let j = 0; j < N_H; j++)
for (let k = 0; k < N_IN; k++)
if (!mask.W1[j][k]) ticket.W1[j][k] = 0;
for (let j = 0; j < N_H; j++)
if (!mask.W2[0][j]) ticket.W2[0][j] = 0;
setStatus(fmt('Retraining winning ticket (%v%/%total% weights)…', { v: total - nPrune, total: total }), 'run');
const EPOCHS = 300;
let epoch = 0;
function step() {
for (let i = 0; i < 20; i++) trainStep(ticket, 0.8, mask);
epoch += 20;
render(ticket);
if (epoch < EPOCHS) {
requestAnimationFrame(step);
} else {
ticketNet = cloneWeights(ticket);
const accFull = (accuracy(trainedNet) * 100).toFixed(1);
const accTicket = (accuracy(ticketNet) * 100).toFixed(1);
const kept = total - nPrune;
setStatus(
fmt('Ticket accuracy: %accTicket%% vs full: %accFull%%%v%', { accTicket: accTicket, accFull: accFull, v: ' — ' + (parseFloat(accTicket) >= parseFloat(accFull) - 5 ? 'winning ticket! 🎫' : 'ticket lost this time.') }),
parseFloat(accTicket) >= parseFloat(accFull) - 5 ? 'ok' : ''
);
setStats(fmt('%kept%/%total% weights kept (%round%% pruned). Size reduction: %round2%%.', { kept: kept, total: total, round: Math.round(ratio * 100), round2: Math.round((1 - kept / total) * 100) }));
btnPrune.disabled = false;
}
}
requestAnimationFrame(step);
}
btnTrain.addEventListener('click', doTrain);
btnPrune.addEventListener('click', doPrune);
btnReset.addEventListener('click', doReset);
document.getElementById('prune-slider').addEventListener('input', function() {
document.getElementById('prune-val').textContent = this.value + '%';
});
// Initial render
doReset();
render(zeroNet());
Notice that the sparse subnetwork, reset to its lucky initial values, reaches accuracy close to the full network — the winning ticket was there from the beginning. Crank up the pruning ratio to strip away more weights and watch when the ticket finally stops winning.
Comments
Loading comments...