The idea
Labeling costs time and money, and not every label is equally useful. If a spam filter is already sure a message about cheap watches is spam, confirming it teaches the filter almost nothing. A message it scores at 50/50 is different: whichever way a person labels it, the model learns something it didn’t know.
The loop
- Label a small random sample, a few hundred examples, and train a first model.
- Score every unlabeled example with the model.
- Send the ones it’s least sure about to people to label.
- Retrain with the new labels, and repeat until the score stops improving or the budget runs out.
“Least sure” can be measured in a few ways. The simplest is the margin: the gap between the model’s top two probabilities. A gap of 0.02 means the model is torn between two labels; a gap of 0.9 means it’s confident.
Try it
This replays labeling on a small, already-labeled dataset of handwritten digits that comes with scikit-learn. It starts from 30 random labels and adds 20 at a time, once picked at random and once picked by smallest margin.
CodeLeast-sure picking against random picking
# pip install scikit-learn numpy
import numpy as np
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
X, y = load_digits(return_X_y=True) # 1,797 small images of handwritten digits, already labeled
X_pool, X_test, y_pool, y_test = train_test_split(X / 16, y, test_size=0.3, stratify=y, random_state=0)
def run(strategy, start=30, step=20, rounds=10):
rng = np.random.default_rng(0)
labeled = list(rng.choice(len(X_pool), start, replace=False))
for _ in range(rounds):
model = LogisticRegression(max_iter=2000).fit(X_pool[labeled], y_pool[labeled])
rest = np.setdiff1d(np.arange(len(X_pool)), labeled)
if strategy == "least sure":
top2 = np.sort(model.predict_proba(X_pool[rest]), axis=1)[:, -2:]
pick = rest[np.argsort(top2[:, 1] - top2[:, 0])[:step]]
else:
pick = rng.choice(rest, step, replace=False)
labeled += list(pick)
model = LogisticRegression(max_iter=2000).fit(X_pool[labeled], y_pool[labeled])
return len(labeled), model.score(X_test, y_test)
for strategy in ["random", "least sure"]:
n, acc = run(strategy)
print(f"{strategy:>10}: {acc:.1%} accuracy after labeling {n} images")With the same 230 labels, least-sure picking usually ends one to five points ahead. Change the random_state values to see how much the gap varies.
Where it goes wrong
- The labeled set stops looking like real data. Uncertain examples can all come from one confusing corner. Mix some random examples into every batch, and always pick the test set at random.
- Near-duplicates. Ten copies of the same confusing message all look uncertain. Remove duplicates before picking.
- Noise looks interesting. Unreadable or junk examples are uncertain too, and labeling them teaches nothing. Let labelers mark them as junk and skip them.
- Small gains on easy tasks. If random labels already reach a high score quickly, the loop adds complexity for little benefit.
When it’s worth it
Active learning pays off when labels are expensive, such as when experts label medical images or legal documents, and when the interesting cases are rare. When labels are cheap or already exist, simply label more at random. Before relying on it, replay it on data you’ve already labeled, as in the code above, to check it helps on your data.