Introduction

Imagine you are teaching a child to recognise cats and dogs. You could point randomly at pictures all day, or you could focus on the confusing ones โ€” the tiny fluffy puppy, the enormous shaggy cat โ€” and let the child ask "wait, what is this one?" That targeted exchange is the spirit of active learning.

In machine learning the same bottleneck appears constantly: labels are expensive. Every medical scan needs a radiologist; every legal document needs a lawyer; every satellite image needs an expert. Models can process millions of unlabelled examples but need labelled ones to learn from. Labelling everything is often impossible.

Active learning flips the usual pipeline. Instead of handing the model a fixed labelled dataset, you give it a large pool of unlabelled examples and let it choose which ones to ask about. A good query strategy zeroes in on the examples that will move the decision boundary the most โ€” and in practice this can reach the same accuracy as random sampling with five to ten times fewer labels.

The idea is not exotic. It dates to the early 1990s (Cohn, Atlas & Ladner, 1994 โ€” Improving generalization with active learning) and the intuition is simple: don't waste the annotator's time on examples the model already handles confidently. Ask only about the hard cases near the boundary between classes.

Query the Boundary

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> &nbsp; Random: <b id="rLabels">0</b></span><br>
  <span>Accuracy โ€” Active: <b id="aAcc">โ€”</b> &nbsp; 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);
Result
Console

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.

The Theory

Why does querying near the boundary work? The formal picture comes from version spaces (Mitchell, 1982): the set of all classifiers still consistent with the labelled data. Every new label shrinks the version space. A query at the boundary โ€” where the model is maximally uncertain โ€” can halve the version space in the best case, giving logarithmic label complexity rather than the linear growth of random sampling.

Three main query strategies are used in practice:

  • Uncertainty sampling โ€” query the point the model is least confident about (e.g. the one whose predicted class probability is closest to 0.5 for a binary classifier). Simple, fast, and the strategy used in the demo above.
  • Query-by-committee (QBC) โ€” train a committee of models on the labelled data; query the point they disagree on most. This reduces the query dependence on a single model's calibration.
  • Expected model change / expected error reduction โ€” query the point that would most reduce the model's expected future error or most change its parameters. Theoretically principled but computationally expensive.

Formal guarantees connect active learning to PAC learning. Under the realizable setting (true function in the hypothesis class), some active learning algorithms achieve exponentially fewer labels than passive learning โ€” a result proven rigorously by Balcan, Beygelzimer & Langford (2006). In the agnostic (noisy) setting the gains are smaller but still substantial for many function classes.

The main caveat is distribution shift: if the queried points cluster near the boundary and the final test distribution is different, the model can be less accurate than expected. This makes active learning most reliable when the unlabelled pool is representative of the deployment domain.

Where It Matters

Any domain where expert annotation is scarce is a natural home for active learning:

  • Medical imaging: training a tumour detector on MRI scans requires radiologist time. Active learning systems (like those built on top of deep learning) can identify the scans where the model is most uncertain and ask only about those, dramatically reducing annotation hours.
  • Natural language processing: labelling sentiment, intent, or named entities in millions of documents is prohibitive. Active learning pipelines query the most ambiguous sentences, keeping annotator fatigue and cost low.
  • Scientific discovery: in drug screening, each wet-lab assay costs time and money. Active learning guides chemists toward the most informative molecules to test, accelerating the search for active compounds.
  • Autonomous robotics: a self-driving system encountering a rare road scenario can flag it for human review โ€” actively requesting labels for the edge cases it hasn't mastered.
  • Low-resource languages: building NLP tools for languages with few speakers often means very limited labelled data. Active learning stretches small annotation budgets much further.

In all these settings the theme is the same: the model knows what it doesn't know, and it asks about exactly those cases.

Conclusion

Active learning is a reminder that data quality beats data quantity. A model that asks targeted questions near its own uncertainty can match โ€” and often surpass โ€” one trained on far larger randomly labelled sets. That efficiency matters whenever annotation is expensive, slow, or scarce.

The approach also has a philosophical flavour: it captures something true about good learning in general. We don't master a subject by reviewing what we already know โ€” we seek out the problems we can barely solve, the boundary between understanding and confusion. Active learning is that strategy, formalised and automated.

For a broader lens on what machine learning can and cannot learn efficiently, see PAC learning โ€” the theory that asks how many examples any learning algorithm needs before it can guarantee generalisation.

Share this article

Pick a channel โ€” or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/active-learning/Content licensed under CC BY-NC 4.0.