<div class="controls">
<label class="toggle-label">
<span>Placing:</span>
<button id="classToggle" type="button" class="class-btn class0">Blue (0)</button>
</label>
<button id="trainBtn" type="button">Train</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="420" height="320"></canvas>
<div id="info" class="info">Add points, then click Train.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
.toggle-label { display: flex; align-items: center; gap: .4rem; font-size: .9rem; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border-radius: 7px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; }
.class-btn.class0 { background: #3a86ff; border-color: #2a6fd0; }
.class-btn.class1 { background: #e63946; border-color: #c92f3c; }
#canvas { border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair; display: block;
max-width: 100%; }
.info { font-size: .88rem; color: #444; margin-top: .45rem; min-height: 1.3em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Logistic regression demo: place 2 classes of 2-D points, train with gradient descent on log-loss (binary cross-entropy), draw the sigmoid decision boundary.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
let currentClass = 0;
let points = []; // {x, y, cls} x,y in [0,1]
// โโ sigmoid โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function sigmoid(z) { return 1 / (1 + Math.exp(-z)); }
// โโ model params โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let w1 = 0, w2 = 0, b = 0; // w = [w1, w2], b = bias
function predict(x, y) { return sigmoid(w1 * x + w2 * y + b); }
// โโ training: gradient descent on binary cross-entropy โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function train() {
if (points.length < 2) { setInfo('Add at least 2 points first.'); return; }
// Check we have both classes
const cls0 = points.filter(p => p.cls === 0);
const cls1 = points.filter(p => p.cls === 1);
if (!cls0.length || !cls1.length) { setInfo('Add points of BOTH classes.'); return; }
w1 = 0; w2 = 0; b = 0;
const lr = 0.5, epochs = 3000;
for (let e = 0; e < epochs; e++) {
let g1 = 0, g2 = 0, gb = 0;
for (const p of points) {
const pred = predict(p.x, p.y);
const err = pred - p.cls; // gradient of log-loss w.r.t. logit
g1 += err * p.x;
g2 += err * p.y;
gb += err;
}
const n = points.length;
w1 -= lr * g1 / n;
w2 -= lr * g2 / n;
b -= lr * gb / n;
}
// โโ compute final log-loss โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let loss = 0;
for (const p of points) {
const pred = Math.max(1e-12, Math.min(1-1e-12, predict(p.x, p.y)));
loss -= p.cls * Math.log(pred) + (1 - p.cls) * Math.log(1 - pred);
}
loss /= points.length;
draw();
setInfo(fmt('Trained %epochs% steps ยท log-loss = %loss% ยท w=[%w1%, %w2%] b=%b%', { epochs: epochs, loss: loss.toFixed(4), w1: w1.toFixed(2), w2: w2.toFixed(2), b: b.toFixed(2) }));
console.log(fmt('logistic-regression: w1=%w1% w2=%w2% b=%b% loss=%loss%', { w1: w1, w2: w2, b: b, loss: loss }));
}
// โโ draw โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function draw() {
ctx.clearRect(0, 0, W, H);
// background: shade by predicted probability if model is non-trivial
if (w1 !== 0 || w2 !== 0 || b !== 0) {
const imgData = ctx.createImageData(W, H);
for (let px = 0; px < W; px++) {
for (let py = 0; py < H; py++) {
const nx = px / W, ny = py / H;
const p = predict(nx, ny);
const i = (py * W + px) * 4;
// blue tint for p<0.5, red tint for p>0.5
if (p < 0.5) {
const t = 1 - p * 2; // 0..1
imgData.data[i] = Math.round(200 + t * 30);
imgData.data[i+1] = Math.round(215 + t * 15);
imgData.data[i+2] = Math.round(240 + t * 15);
imgData.data[i+3] = 200;
} else {
const t = (p - 0.5) * 2;
imgData.data[i] = Math.round(240 + t * 15);
imgData.data[i+1] = Math.round(210 - t * 30);
imgData.data[i+2] = Math.round(215 - t * 40);
imgData.data[i+3] = 200;
}
}
}
ctx.putImageData(imgData, 0, 0);
// decision boundary: p=0.5 โ w1*x + w2*y + b = 0
ctx.beginPath();
ctx.strokeStyle = '#0a7d33';
ctx.lineWidth = 2.5;
if (Math.abs(w2) > 1e-6) {
// y = -(w1*x + b) / w2 for x in [0,1]
const x0 = 0, y0 = -(w1 * x0 + b) / w2;
const x1 = 1, y1 = -(w1 * x1 + b) / w2;
ctx.moveTo(x0 * W, y0 * H);
ctx.lineTo(x1 * W, y1 * H);
} else if (Math.abs(w1) > 1e-6) {
const xb = -b / w1;
ctx.moveTo(xb * W, 0);
ctx.lineTo(xb * W, H);
}
ctx.stroke();
}
// points
for (const p of points) {
ctx.beginPath();
ctx.arc(p.x * W, p.y * H, 7, 0, 2 * Math.PI);
ctx.fillStyle = p.cls === 0 ? '#3a86ff' : '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
function setInfo(t) { document.getElementById('info').textContent = t; }
// โโ events โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX / W;
const py = (e.clientY - rect.top) * scaleY / H;
points.push({ x: px, y: py, cls: currentClass });
draw();
setInfo(fmt('%length% point(s) placed. Click Train to fit the model.', { length: points.length }));
});
document.getElementById('classToggle').addEventListener('click', () => {
currentClass = 1 - currentClass;
const btn = document.getElementById('classToggle');
btn.textContent = currentClass === 0 ? 'Blue (0)' : 'Red (1)';
btn.className = 'class-btn class' + currentClass;
});
document.getElementById('trainBtn').addEventListener('click', train);
document.getElementById('resetBtn').addEventListener('click', () => {
points = []; w1 = 0; w2 = 0; b = 0;
draw();
setInfo('Add points, then click Train.');
});
draw();
<div class="controls">
<label class="toggle-label">
<span>Placing:</span>
<button id="classToggle" type="button" class="class-btn class0">Blue (0)</button>
</label>
<button id="trainBtn" type="button">Train</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="420" height="320"></canvas>
<div id="info" class="info">Add points, then click Train.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
.toggle-label { display: flex; align-items: center; gap: .4rem; font-size: .9rem; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border-radius: 7px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.ghost { background: #fff; color: #1d3557; }
.class-btn.class0 { background: #3a86ff; border-color: #2a6fd0; }
.class-btn.class1 { background: #e63946; border-color: #c92f3c; }
#canvas { border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair; display: block;
max-width: 100%; }
.info { font-size: .88rem; color: #444; margin-top: .45rem; min-height: 1.3em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Logistic regression demo: place 2 classes of 2-D points, train with gradient descent on log-loss (binary cross-entropy), draw the sigmoid decision boundary.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
let currentClass = 0;
let points = []; // {x, y, cls} x,y in [0,1]
// โโ sigmoid โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function sigmoid(z) { return 1 / (1 + Math.exp(-z)); }
// โโ model params โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let w1 = 0, w2 = 0, b = 0; // w = [w1, w2], b = bias
function predict(x, y) { return sigmoid(w1 * x + w2 * y + b); }
// โโ training: gradient descent on binary cross-entropy โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function train() {
if (points.length < 2) { setInfo('Add at least 2 points first.'); return; }
// Check we have both classes
const cls0 = points.filter(p => p.cls === 0);
const cls1 = points.filter(p => p.cls === 1);
if (!cls0.length || !cls1.length) { setInfo('Add points of BOTH classes.'); return; }
w1 = 0; w2 = 0; b = 0;
const lr = 0.5, epochs = 3000;
for (let e = 0; e < epochs; e++) {
let g1 = 0, g2 = 0, gb = 0;
for (const p of points) {
const pred = predict(p.x, p.y);
const err = pred - p.cls; // gradient of log-loss w.r.t. logit
g1 += err * p.x;
g2 += err * p.y;
gb += err;
}
const n = points.length;
w1 -= lr * g1 / n;
w2 -= lr * g2 / n;
b -= lr * gb / n;
}
// โโ compute final log-loss โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let loss = 0;
for (const p of points) {
const pred = Math.max(1e-12, Math.min(1-1e-12, predict(p.x, p.y)));
loss -= p.cls * Math.log(pred) + (1 - p.cls) * Math.log(1 - pred);
}
loss /= points.length;
draw();
setInfo(fmt('Trained %epochs% steps ยท log-loss = %loss% ยท w=[%w1%, %w2%] b=%b%', { epochs: epochs, loss: loss.toFixed(4), w1: w1.toFixed(2), w2: w2.toFixed(2), b: b.toFixed(2) }));
console.log(fmt('logistic-regression: w1=%w1% w2=%w2% b=%b% loss=%loss%', { w1: w1, w2: w2, b: b, loss: loss }));
}
// โโ draw โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function draw() {
ctx.clearRect(0, 0, W, H);
// background: shade by predicted probability if model is non-trivial
if (w1 !== 0 || w2 !== 0 || b !== 0) {
const imgData = ctx.createImageData(W, H);
for (let px = 0; px < W; px++) {
for (let py = 0; py < H; py++) {
const nx = px / W, ny = py / H;
const p = predict(nx, ny);
const i = (py * W + px) * 4;
// blue tint for p<0.5, red tint for p>0.5
if (p < 0.5) {
const t = 1 - p * 2; // 0..1
imgData.data[i] = Math.round(200 + t * 30);
imgData.data[i+1] = Math.round(215 + t * 15);
imgData.data[i+2] = Math.round(240 + t * 15);
imgData.data[i+3] = 200;
} else {
const t = (p - 0.5) * 2;
imgData.data[i] = Math.round(240 + t * 15);
imgData.data[i+1] = Math.round(210 - t * 30);
imgData.data[i+2] = Math.round(215 - t * 40);
imgData.data[i+3] = 200;
}
}
}
ctx.putImageData(imgData, 0, 0);
// decision boundary: p=0.5 โ w1*x + w2*y + b = 0
ctx.beginPath();
ctx.strokeStyle = '#0a7d33';
ctx.lineWidth = 2.5;
if (Math.abs(w2) > 1e-6) {
// y = -(w1*x + b) / w2 for x in [0,1]
const x0 = 0, y0 = -(w1 * x0 + b) / w2;
const x1 = 1, y1 = -(w1 * x1 + b) / w2;
ctx.moveTo(x0 * W, y0 * H);
ctx.lineTo(x1 * W, y1 * H);
} else if (Math.abs(w1) > 1e-6) {
const xb = -b / w1;
ctx.moveTo(xb * W, 0);
ctx.lineTo(xb * W, H);
}
ctx.stroke();
}
// points
for (const p of points) {
ctx.beginPath();
ctx.arc(p.x * W, p.y * H, 7, 0, 2 * Math.PI);
ctx.fillStyle = p.cls === 0 ? '#3a86ff' : '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
function setInfo(t) { document.getElementById('info').textContent = t; }
// โโ events โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX / W;
const py = (e.clientY - rect.top) * scaleY / H;
points.push({ x: px, y: py, cls: currentClass });
draw();
setInfo(fmt('%length% point(s) placed. Click Train to fit the model.', { length: points.length }));
});
document.getElementById('classToggle').addEventListener('click', () => {
currentClass = 1 - currentClass;
const btn = document.getElementById('classToggle');
btn.textContent = currentClass === 0 ? 'Blue (0)' : 'Red (1)';
btn.className = 'class-btn class' + currentClass;
});
document.getElementById('trainBtn').addEventListener('click', train);
document.getElementById('resetBtn').addEventListener('click', () => {
points = []; w1 = 0; w2 = 0; b = 0;
draw();
setInfo('Add points, then click Train.');
});
draw();
Notice that with well-separated points the boundary sits cleanly between them. Overlap the classes and the boundary settles somewhere in between โ logistic regression never claims certainty, it gives you a probability. Click Reset to start fresh.
Comments
Loading comments...