Below you will see two classes of points (blue and orange) with many unlabelled examples (gray). Two strategies compete: active (queries the point nearest the current decision boundary) and random (picks any unlabelled point at random). Press Query 1 point to advance one step at a time or Run 20 steps to see the divergence clearly.
<p class="hint">Two classes (blue / orange) separated by a decision boundary. Gray dots are unlabelled. <b>Active</b> queries the point nearest the current boundary; <b>Random</b> picks at random. Watch accuracy diverge.</p>
<canvas id="cv" width="340" height="260"></canvas>
<div class="stats">
<span>Labels used โ Active: <b id="aLabels">0</b> Random: <b id="rLabels">0</b></span><br>
<span>Accuracy โ Active: <b id="aAcc">โ</b> Random: <b id="rAcc">โ</b></span>
</div>
<div class="btns">
<button id="step1" type="button">Query 1 point</button>
<button id="run20" type="button">Run 20 steps</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f8fafc; max-width: 100%; }
.stats { font-size: .9rem; margin: .55rem 0 .45rem; line-height: 1.7; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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; }
// Active Learning demo โ uncertainty sampling vs random sampling.
// Each "model" is a linear classifier (logistic regression gradient descent)
// trained on the labelled set so far. The active strategy always queries the
// unlabelled point closest to the decision boundary (|wx+b| minimal).
const W = 340, H = 260, N_POOL = 80;
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
// โโ data generation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// True boundary: diagonal line y = x (in [0,1]ยฒ normalised coords).
// Class 1 (blue) = above, Class 0 (orange) = below. Add Gaussian noise.
function rand() { return Math.random(); }
function randn() {
let u=0,v=0;
while(!u) u=rand(); while(!v) v=rand();
return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);
}
let pool = []; // all unlabelled points
let aLabelled = []; // {x,y,label} labelled by active strategy
let rLabelled = []; // labelled by random strategy
let aW=[0.1,0.1], aB=0; // active model weights (wx, wy, bias)
let rW=[0.1,0.1], rB=0;
function genData() {
pool = [];
for (let i = 0; i < N_POOL; i++) {
let px = rand(), py = rand();
// label = 1 if py > px (above diagonal), 0 otherwise, with small noise
let label = (py - px + 0.05*randn()) > 0 ? 1 : 0;
pool.push({ x: px, y: py, label });
}
aLabelled = []; rLabelled = [];
aW=[0.1,0.1]; aB=0;
rW=[0.1,0.1]; rB=0;
// Seed each with 2 initial labelled points (one per class)
let cls0 = pool.filter(p=>p.label===0), cls1 = pool.filter(p=>p.label===1);
function seedOne(arr, lbl) {
let idx = Math.floor(rand()*arr.length);
let pt = arr[idx];
pool = pool.filter(p=>p!==pt);
return {...pt};
}
aLabelled.push(seedOne(cls0,0)); aLabelled.push(seedOne(cls1,1));
// find same-index-ish points for random (pick two from pool)
let r0 = pool.filter(p=>p.label===0)[0];
let r1 = pool.filter(p=>p.label===1)[0];
rLabelled.push({...r0}); rLabelled.push({...r1});
}
// โโ logistic regression (mini gradient descent) โโโโโโโโโโโโโโโโโโโโโโโ
function sigmoid(z){ return 1/(1+Math.exp(-z)); }
function score(w,b,pt){ return w[0]*pt.x + w[1]*pt.y + b; }
function trainModel(labelled) {
let w=[0,0], b=0;
const lr=0.8, iters=200;
for(let it=0;it<iters;it++){
let dw=[0,0], db=0;
for(const pt of labelled){
let z=score(w,b,pt), p=sigmoid(z), err=p-pt.label;
dw[0]+=err*pt.x; dw[1]+=err*pt.y; db+=err;
}
const n=labelled.length;
w[0]-=lr*dw[0]/n; w[1]-=lr*dw[1]/n; b-=lr*db/n;
}
return {w, b};
}
// โโ query strategies โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function unlabelledPool() {
// points not yet labelled by either strategy
const aSet = new Set(aLabelled.map(p=>`${p.x},${p.y}`));
const rSet = new Set(rLabelled.map(p=>`${p.x},${p.y}`));
return pool.filter(p => !aSet.has(`${p.x},${p.y}`) && !rSet.has(`${p.x},${p.y}`));
}
function queryActive(w, b) {
// uncertainty sampling: point with smallest |score| = nearest boundary
let cands = unlabelledPool();
if(!cands.length) return null;
let best=null, bestDist=Infinity;
for(const pt of cands){
let d=Math.abs(score(w,b,pt));
if(d<bestDist){ bestDist=d; best=pt; }
}
return best;
}
function queryRandom() {
let cands = unlabelledPool();
if(!cands.length) return null;
return cands[Math.floor(rand()*cands.length)];
}
// โโ accuracy on remaining unlabelled pool โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function accuracy(w, b) {
let cands = unlabelledPool();
if(!cands.length) return null;
let correct=0;
for(const pt of cands){
let pred = score(w,b,pt)>=0 ? 1 : 0;
if(pred===pt.label) correct++;
}
return (correct/cands.length*100).toFixed(1)+'%';
}
// โโ drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function toCanvas(px,py){ return [px*W, (1-py)*H]; }
function drawBoundary(w, b, color) {
// wx*x + wy*y + b = 0 => y = (-wx*x - b)/wy
ctx.strokeStyle=color; ctx.lineWidth=1.5; ctx.setLineDash([5,4]);
ctx.beginPath();
let x0=0, y0=(-w[0]*0 - b)/w[1];
let x1=1, y1=(-w[0]*1 - b)/w[1];
let [cx0,cy0]=toCanvas(x0,y0), [cx1,cy1]=toCanvas(x1,y1);
ctx.moveTo(cx0,cy0); ctx.lineTo(cx1,cy1); ctx.stroke();
ctx.setLineDash([]);
}
function drawAll(aw,ab,rw,rb){
ctx.clearRect(0,0,W,H);
// draw pool (gray)
const aSet = new Set(aLabelled.map(p=>`${p.x},${p.y}`));
const rSet = new Set(rLabelled.map(p=>`${p.x},${p.y}`));
for(const pt of pool){
const [cx,cy]=toCanvas(pt.x,pt.y);
const isAL = aSet.has(`${pt.x},${pt.y}`);
const isRL = rSet.has(`${pt.x},${pt.y}`);
if(isAL||isRL){
// labelled โ show true color
ctx.fillStyle = pt.label===1 ? '#3a86ff' : '#ff8c42';
ctx.beginPath(); ctx.arc(cx,cy,5,0,Math.PI*2); ctx.fill();
if(isAL&&!isRL){ ctx.strokeStyle='#1d3557'; ctx.lineWidth=1.5; ctx.stroke(); }
if(isRL&&!isAL){ ctx.strokeStyle='#8b2fc9'; ctx.lineWidth=1.5; ctx.stroke(); }
if(isAL&&isRL){ ctx.strokeStyle='#555'; ctx.lineWidth=1; ctx.stroke(); }
} else {
// unlabelled โ gray
ctx.fillStyle='#bcc0c6';
ctx.beginPath(); ctx.arc(cx,cy,4,0,Math.PI*2); ctx.fill();
}
}
// boundaries
if(aLabelled.length>=2) drawBoundary(aw,ab,'#1d6fb5');
if(rLabelled.length>=2) drawBoundary(rw,rb,'#8b2fc9');
// legend
ctx.font='11px system-ui'; ctx.textAlign='left';
ctx.fillStyle='#1d6fb5'; ctx.fillRect(8,8,18,2);
ctx.fillStyle='#1d6fb5'; ctx.fillText('Active boundary',30,13);
ctx.fillStyle='#8b2fc9'; ctx.fillRect(8,22,18,2);
ctx.fillStyle='#8b2fc9'; ctx.fillText('Random boundary',30,27);
}
function updateUI(){
let am = trainModel(aLabelled), rm = trainModel(rLabelled);
aW=am.w; aB=am.b; rW=rm.w; rB=rm.b;
drawAll(aW,aB,rW,rB);
document.getElementById('aLabels').textContent = aLabelled.length;
document.getElementById('rLabels').textContent = rLabelled.length;
document.getElementById('aAcc').textContent = accuracy(aW,aB) || 'โ';
document.getElementById('rAcc').textContent = accuracy(rW,rB) || 'โ';
}
function step(){
let aq = queryActive(aW,aB);
let rq = queryRandom();
if(aq){ aLabelled.push({...aq}); }
if(rq){ rLabelled.push({...rq}); }
updateUI();
}
document.getElementById('step1').addEventListener('click', step);
document.getElementById('run20').addEventListener('click', ()=>{ for(let i=0;i<20;i++) step(); });
document.getElementById('reset').addEventListener('click', ()=>{ genData(); updateUI(); });
// init
genData();
updateUI();
// Verification log (for node check)
console.log('Pool size:', pool.length, 'Active labelled:', aLabelled.length, 'Random labelled:', rLabelled.length);
<p class="hint">Two classes (blue / orange) separated by a decision boundary. Gray dots are unlabelled. <b>Active</b> queries the point nearest the current boundary; <b>Random</b> picks at random. Watch accuracy diverge.</p>
<canvas id="cv" width="340" height="260"></canvas>
<div class="stats">
<span>Labels used โ Active: <b id="aLabels">0</b> Random: <b id="rLabels">0</b></span><br>
<span>Accuracy โ Active: <b id="aAcc">โ</b> Random: <b id="rAcc">โ</b></span>
</div>
<div class="btns">
<button id="step1" type="button">Query 1 point</button>
<button id="run20" type="button">Run 20 steps</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f8fafc; max-width: 100%; }
.stats { font-size: .9rem; margin: .55rem 0 .45rem; line-height: 1.7; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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; }
// Active Learning demo โ uncertainty sampling vs random sampling.
// Each "model" is a linear classifier (logistic regression gradient descent)
// trained on the labelled set so far. The active strategy always queries the
// unlabelled point closest to the decision boundary (|wx+b| minimal).
const W = 340, H = 260, N_POOL = 80;
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
// โโ data generation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// True boundary: diagonal line y = x (in [0,1]ยฒ normalised coords).
// Class 1 (blue) = above, Class 0 (orange) = below. Add Gaussian noise.
function rand() { return Math.random(); }
function randn() {
let u=0,v=0;
while(!u) u=rand(); while(!v) v=rand();
return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);
}
let pool = []; // all unlabelled points
let aLabelled = []; // {x,y,label} labelled by active strategy
let rLabelled = []; // labelled by random strategy
let aW=[0.1,0.1], aB=0; // active model weights (wx, wy, bias)
let rW=[0.1,0.1], rB=0;
function genData() {
pool = [];
for (let i = 0; i < N_POOL; i++) {
let px = rand(), py = rand();
// label = 1 if py > px (above diagonal), 0 otherwise, with small noise
let label = (py - px + 0.05*randn()) > 0 ? 1 : 0;
pool.push({ x: px, y: py, label });
}
aLabelled = []; rLabelled = [];
aW=[0.1,0.1]; aB=0;
rW=[0.1,0.1]; rB=0;
// Seed each with 2 initial labelled points (one per class)
let cls0 = pool.filter(p=>p.label===0), cls1 = pool.filter(p=>p.label===1);
function seedOne(arr, lbl) {
let idx = Math.floor(rand()*arr.length);
let pt = arr[idx];
pool = pool.filter(p=>p!==pt);
return {...pt};
}
aLabelled.push(seedOne(cls0,0)); aLabelled.push(seedOne(cls1,1));
// find same-index-ish points for random (pick two from pool)
let r0 = pool.filter(p=>p.label===0)[0];
let r1 = pool.filter(p=>p.label===1)[0];
rLabelled.push({...r0}); rLabelled.push({...r1});
}
// โโ logistic regression (mini gradient descent) โโโโโโโโโโโโโโโโโโโโโโโ
function sigmoid(z){ return 1/(1+Math.exp(-z)); }
function score(w,b,pt){ return w[0]*pt.x + w[1]*pt.y + b; }
function trainModel(labelled) {
let w=[0,0], b=0;
const lr=0.8, iters=200;
for(let it=0;it<iters;it++){
let dw=[0,0], db=0;
for(const pt of labelled){
let z=score(w,b,pt), p=sigmoid(z), err=p-pt.label;
dw[0]+=err*pt.x; dw[1]+=err*pt.y; db+=err;
}
const n=labelled.length;
w[0]-=lr*dw[0]/n; w[1]-=lr*dw[1]/n; b-=lr*db/n;
}
return {w, b};
}
// โโ query strategies โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function unlabelledPool() {
// points not yet labelled by either strategy
const aSet = new Set(aLabelled.map(p=>`${p.x},${p.y}`));
const rSet = new Set(rLabelled.map(p=>`${p.x},${p.y}`));
return pool.filter(p => !aSet.has(`${p.x},${p.y}`) && !rSet.has(`${p.x},${p.y}`));
}
function queryActive(w, b) {
// uncertainty sampling: point with smallest |score| = nearest boundary
let cands = unlabelledPool();
if(!cands.length) return null;
let best=null, bestDist=Infinity;
for(const pt of cands){
let d=Math.abs(score(w,b,pt));
if(d<bestDist){ bestDist=d; best=pt; }
}
return best;
}
function queryRandom() {
let cands = unlabelledPool();
if(!cands.length) return null;
return cands[Math.floor(rand()*cands.length)];
}
// โโ accuracy on remaining unlabelled pool โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function accuracy(w, b) {
let cands = unlabelledPool();
if(!cands.length) return null;
let correct=0;
for(const pt of cands){
let pred = score(w,b,pt)>=0 ? 1 : 0;
if(pred===pt.label) correct++;
}
return (correct/cands.length*100).toFixed(1)+'%';
}
// โโ drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function toCanvas(px,py){ return [px*W, (1-py)*H]; }
function drawBoundary(w, b, color) {
// wx*x + wy*y + b = 0 => y = (-wx*x - b)/wy
ctx.strokeStyle=color; ctx.lineWidth=1.5; ctx.setLineDash([5,4]);
ctx.beginPath();
let x0=0, y0=(-w[0]*0 - b)/w[1];
let x1=1, y1=(-w[0]*1 - b)/w[1];
let [cx0,cy0]=toCanvas(x0,y0), [cx1,cy1]=toCanvas(x1,y1);
ctx.moveTo(cx0,cy0); ctx.lineTo(cx1,cy1); ctx.stroke();
ctx.setLineDash([]);
}
function drawAll(aw,ab,rw,rb){
ctx.clearRect(0,0,W,H);
// draw pool (gray)
const aSet = new Set(aLabelled.map(p=>`${p.x},${p.y}`));
const rSet = new Set(rLabelled.map(p=>`${p.x},${p.y}`));
for(const pt of pool){
const [cx,cy]=toCanvas(pt.x,pt.y);
const isAL = aSet.has(`${pt.x},${pt.y}`);
const isRL = rSet.has(`${pt.x},${pt.y}`);
if(isAL||isRL){
// labelled โ show true color
ctx.fillStyle = pt.label===1 ? '#3a86ff' : '#ff8c42';
ctx.beginPath(); ctx.arc(cx,cy,5,0,Math.PI*2); ctx.fill();
if(isAL&&!isRL){ ctx.strokeStyle='#1d3557'; ctx.lineWidth=1.5; ctx.stroke(); }
if(isRL&&!isAL){ ctx.strokeStyle='#8b2fc9'; ctx.lineWidth=1.5; ctx.stroke(); }
if(isAL&&isRL){ ctx.strokeStyle='#555'; ctx.lineWidth=1; ctx.stroke(); }
} else {
// unlabelled โ gray
ctx.fillStyle='#bcc0c6';
ctx.beginPath(); ctx.arc(cx,cy,4,0,Math.PI*2); ctx.fill();
}
}
// boundaries
if(aLabelled.length>=2) drawBoundary(aw,ab,'#1d6fb5');
if(rLabelled.length>=2) drawBoundary(rw,rb,'#8b2fc9');
// legend
ctx.font='11px system-ui'; ctx.textAlign='left';
ctx.fillStyle='#1d6fb5'; ctx.fillRect(8,8,18,2);
ctx.fillStyle='#1d6fb5'; ctx.fillText('Active boundary',30,13);
ctx.fillStyle='#8b2fc9'; ctx.fillRect(8,22,18,2);
ctx.fillStyle='#8b2fc9'; ctx.fillText('Random boundary',30,27);
}
function updateUI(){
let am = trainModel(aLabelled), rm = trainModel(rLabelled);
aW=am.w; aB=am.b; rW=rm.w; rB=rm.b;
drawAll(aW,aB,rW,rB);
document.getElementById('aLabels').textContent = aLabelled.length;
document.getElementById('rLabels').textContent = rLabelled.length;
document.getElementById('aAcc').textContent = accuracy(aW,aB) || 'โ';
document.getElementById('rAcc').textContent = accuracy(rW,rB) || 'โ';
}
function step(){
let aq = queryActive(aW,aB);
let rq = queryRandom();
if(aq){ aLabelled.push({...aq}); }
if(rq){ rLabelled.push({...rq}); }
updateUI();
}
document.getElementById('step1').addEventListener('click', step);
document.getElementById('run20').addEventListener('click', ()=>{ for(let i=0;i<20;i++) step(); });
document.getElementById('reset').addEventListener('click', ()=>{ genData(); updateUI(); });
// init
genData();
updateUI();
// Verification log (for node check)
console.log('Pool size:', pool.length, 'Active labelled:', aLabelled.length, 'Random labelled:', rLabelled.length);
Notice how the active learner's boundary stabilises quickly while random sampling still wavers after many more labels. The key metric is accuracy on the unlabelled pool โ the active learner almost always pulls ahead with fewer queries. Each gray dot the active strategy picks is the one that would shift the boundary the most if it turned out to be blue or orange.
Comments
Loading comments...