Problem
A fraud model scores card payments. You trained it on last quarter's data and it worked. Then fraudsters changed tactics, a new merchant category launched, and a holiday shifted spending. The model still answers every request, with the same confidence, and gets steadily worse. Retraining from scratch every night on all history is slow and still averages the old world with the new one.
A second version of the same problem: a news site must choose which of three headlines to show. It only learns whether a headline gets clicks by showing it. Show the current favourite every time and you never find out that another headline is better; show them all equally and you waste traffic on the bad ones. Here the learner's own choices decide which labels it collects.
Without online learning, a model is frozen at the moment it was fitted. Online methods keep it current in constant memory, at the cost of new failure modes: forgetting, sensitivity to the step size, and noisy updates.
Intuition
Think of a thermostat rather than a blueprint. It does not plan the year's heating in advance; it measures, nudges, and measures again. An online learner does the same: for each new example it first predicts, then sees the true answer, scores its mistake, and nudges its weights a little to make that mistake smaller. It never revisits old data. Its memory of the past lives only in its current weights.
How big each nudge is decides everything. Big nudges forget the past quickly, which is good when the world changes and bad when it does not (the model chases noise). Small nudges average over a long history, which is good when the world is stable and bad after a change (the model is stuck in the past). A step size that shrinks over time, the classic choice for a fixed problem, slowly turns the learner into a frozen model.
Bandits add a second dial: explore or exploit. You can only learn how good an option is by trying it. The good strategies try each option enough to be confident, then spend most of their time on the best one, and they keep a little doubt alive for options they have tried only rarely.
Mechanics
The online protocol
At each round t = 1, 2, …, T the learner holds weights wt, receives xt, predicts, then suffers loss ℓt(wt). Online gradient descent updates
wt+1 = wt − ηt ∇ℓt(wt)
which is stochastic gradient descent (see optimizers) with each example used once. Scoring each example before training on it is called prequential evaluation; its running average is an unbiased estimate of the learner's current error, without a held-out set.
Regret
With no assumption that the data are i.i.d., or even random, the yardstick is the best single fixed model chosen with hindsight:
RegretT = Σt=1..T ℓt(wt) − minw Σt=1..T ℓt(w)
An algorithm "learns" if regret is sublinear, so RegretT/T → 0: on average it does as well as the best fixed model. For convex losses with bounded gradients, online gradient descent with ηt ∝ 1/√t has regret O(√T) (Zinkevich, 2003); for strongly convex losses, ηt ∝ 1/t gives O(log T). Regret against a fixed comparator is the wrong yardstick once the data drift; then the comparator is allowed to move, and decaying step sizes are no longer the right choice.
Concept drift
Drift means the joint distribution p(x, y) changes over time. Covariate shift changes p(x) only; concept drift changes p(y | x), the rule itself. It can be sudden, gradual, or recurring (seasons). The standard responses are:
- Forget on purpose. A constant step size weights an example k steps old by roughly (1 − η·c)k, an exponential moving window. A sliding-window refit does the same with a hard edge.
- Detect and reset. Monitor the prequential error; when it rises significantly (tests such as DDM, Page–Hinkley or ADWIN), retrain or raise the step size. A later page on monitoring and drift covers detection in production.
- Ensembles over time. Keep models trained on different periods and weight them by recent accuracy.
partial_fit in scikit-learn
Estimators that can learn incrementally expose partial_fit(X, y).
SGDClassifier.partial_fit
runs one epoch of SGD over the batch it is given (it calls _partial_fit with
max_iter=1), and needs classes= on the first call because a batch may not contain every
label. It keeps the step counter t_ across calls: _partial_fit sets it to 1.0 only if it
does not exist yet (lines 655–656),
and each fit adds the number of samples it processed
(line 776). The schedules read
that counter inside the Cython loop
(_sgd_fast.pyx.tp 457–488):
"optimal", the classifier's default: η = 1 / (α·(t0 + t − 1)), with t0 chosen by Bottou's heuristic so the first step is a sensible size."invscaling", the regressor's default: η =eta0/ tpower_t, witheta0=0.01andpower_t=0.25."constant": η =eta0, which keeps adapting forever."pa1"/"pa2": the passive-aggressive updates (Crammer et al., 2006), which step only as far as needed to fix the current mistake.
Because t_ only grows, a long-running partial_fit loop with a decaying schedule ends up
taking tiny steps: it converges on a stationary stream and stops adapting on a drifting one.
| Component | Shape/Params | Meaning | Source |
|---|---|---|---|
SGDClassifier | loss="hinge", alpha=0.0001, learning_rate="optimal", eta0=0.01, power_t=0.5 | Linear classifier trained by SGD; supports partial_fit. | _stochastic_gradient.py 958–1427 |
SGDRegressor | loss="squared_error", learning_rate="invscaling", eta0=0.01, power_t=0.25 | Linear regressor trained by SGD. | _stochastic_gradient.py 1802–2122 |
partial_fit (classifier) | X [n, d], y [n], classes | One SGD epoch over this batch; classes required on the first call. | 848–909 |
partial_fit (regressor) | X [n, d], y [n] | One SGD epoch over this batch. | 1540–1578 |
t_ | float, starts at 1.0 | Samples seen so far, kept across partial_fit calls; drives decaying schedules. | 1779 |
| step-size schedules | optimal, invscaling, constant | How ηt is computed per sample. | _sgd_fast.pyx.tp 457–488 |
| update with L2 decay | w.scale(1 − η α), then w += update·x | Weight decay applied as a multiplicative shrink each step. | _sgd_fast.pyx.tp 516–542 |
Bandits
A K-armed bandit has arms with unknown mean rewards μ1, …, μK. Each round the learner pulls one arm and sees only that arm's reward. Its (pseudo-)regret after T rounds is
RegretT = Σt (μ* − μat) = Σa Δa · E[Na(T)]
where Δa = μ* − μa is arm a's gap and Na its pull count. Lai and Robbins (1985) showed any consistent policy must pull each bad arm Ω(log T) times, so logarithmic regret is the best possible. Three standard policies:
- ε-greedy. With probability ε pull a random arm, otherwise the arm with the best empirical mean. Simple, but with fixed ε it keeps exploring forever, so its regret grows linearly at ε·(average gap) per round.
- UCB1 (Auer, Cesa-Bianchi and Fischer, 2002). Pull each arm once, then the arm maximizing μ̂a + √(2 ln t / Na). The bonus is large for rarely tried arms ("optimism in the face of uncertainty") and shrinks as evidence accumulates. Regret O(Σa log T / Δa).
- Thompson sampling (Thompson, 1933). Keep a posterior per arm, for 0/1 rewards a Beta(1 + successes, 1 + failures); each round draw one sample from each posterior and pull the arm with the largest draw. It explores in proportion to the probability that each arm is the best, and matches the Lai–Robbins bound for Bernoulli arms.
Contextual bandits (LinUCB, linear Thompson sampling) condition the choice on features xt; full reinforcement learning adds state that the actions change.
Worked example
Every number below comes from tested helpers in ml_models_docs.py with fixed seeds.
A stream that drifts
400 examples arrive one at a time: x uniform on [−1, 1], y = w·x + Gaussian noise (σ = 0.3), with w = 2.0 for the first 200 examples and w = -1.0 after. Three learners fit ŷ = w·x with squared loss, gradient 2(ŷ − y)x, starting from w = 0:
- frozen: least squares on the first 100 examples (w = 2.015), then never updated;
- 1/t step: ηt = 0.5/t;
- constant step: η = 0.1.
The constant-step learner's first three rounds:
| t | x | y | w before | ŷ | loss | w after |
|---|---|---|---|---|---|---|
| 1 | -0.095 | -0.824 | 0.0000 | -0.0000 | 0.6797 | 0.0157 |
| 2 | -0.069 | -0.387 | 0.0157 | -0.0011 | 0.1492 | 0.0210 |
| 3 | 0.016 | -0.132 | 0.0210 | 0.0003 | 0.0176 | 0.0206 |
Round 1: ŷ = 0, so the step is −0.1 × 2 × (0 − (-0.824)) × (-0.095) = 0.0157. The early inputs happen to be near zero, so the weight barely moves: an online learner's progress depends on which examples arrive. Prequential mean squared error in windows of 50:
| Examples | 1–50 | 51–100 | 101–150 | 151–200 | 201–250 | 251–300 | 301–350 | 351–400 |
|---|---|---|---|---|---|---|---|---|
| frozen | 0.088 | 0.087 | 0.095 | 0.112 | 3.107 | 3.227 | 2.326 | 2.710 |
| 1/t step | 0.544 | 0.205 | 0.190 | 0.164 | 2.112 | 1.934 | 1.243 | 1.353 |
| constant step | 0.334 | 0.092 | 0.102 | 0.117 | 0.596 | 0.119 | 0.077 | 0.106 |
Before the drift the frozen model is best (its error, about 0.10, is close to the noise variance 0.09), and the 1/t learner is still converging. After the drift the frozen model's error jumps to 3.11. The 1/t learner has shrunk its step to 0.0025 by round 200, so its weight crawls from 1.56 to only 1.07 by round 400. The constant-step learner pays some noise before the drift but has reached w = -0.85 within 50 rounds of it and ends at -0.97.
Regret on the stable part
On the first 200 examples (no drift), online gradient descent with ηt = 0.5/√t against the best fixed weight chosen with hindsight:
| T | Best fixed w | Learner's total loss | Best total loss | Regret | Regret / T |
|---|---|---|---|---|---|
| 10 | 2.284 | 7.49 | 0.70 | 6.79 | 0.679 |
| 25 | 2.067 | 8.99 | 2.00 | 6.99 | 0.280 |
| 50 | 2.052 | 11.63 | 4.37 | 7.26 | 0.145 |
| 100 | 2.015 | 16.10 | 8.74 | 7.36 | 0.074 |
| 200 | 2.009 | 26.75 | 19.11 | 7.65 | 0.038 |
Almost all of the regret, 6.79, is paid in the first 10 rounds while the weight climbs from 0; afterwards it grows by only 0.86 over 190 rounds. Regret per round falls from 0.679 to 0.038: sublinear regret, the online learner's guarantee. Squared loss here is strongly convex, so the growth is even slower than the √T worst case.
Three headlines: a Bernoulli bandit
Three arms with click rates 0.25, 0.45, 0.55, 1000 rounds, averaged over 50 seeds. Within a seed every policy sees the same reward draws. ε = 0.1; ε-greedy, UCB1 and greedy pull each arm once first, Thompson sampling starts from Beta(1, 1).
| Policy | Regret at 10 | Regret at 50 | Regret at 100 | Regret at 250 | Regret at 500 | Regret at 1000 | Best-arm share |
|---|---|---|---|---|---|---|---|
| greedy | 1.2 | 5.2 | 10.0 | 24.0 | 47.0 | 93.0 | 51% |
| ε-greedy | 1.3 | 4.6 | 8.1 | 15.2 | 23.1 | 35.2 | 76% |
| UCB1 | 1.2 | 4.7 | 8.4 | 17.4 | 29.0 | 45.4 | 68% |
| Thompson | 1.2 | 4.5 | 7.3 | 12.2 | 16.2 | 20.2 | 84% |
Greedy commits to whichever arm looked best after a few pulls; in many seeds that is the wrong one, and its regret grows almost linearly (93.0 after 1000 rounds). Thompson sampling has the least regret, 20.2, and the slope of its curve keeps falling. UCB1 is behind ε-greedy at this horizon (45.4 against 35.2): its √(2 ln t/N) bonus is conservative, so with gaps of 0.1 it keeps checking the second arm for a long time. Its guarantee is asymptotic. ε-greedy's regret never stops growing at the floor of ε × mean gap = 0.0133 per round, while UCB1's per-round regret keeps shrinking like (log t)/t, so over a long enough horizon UCB1 wins.
The first seven UCB1 rounds of seed 5 (bonus computed before the pull):
| t | Pulls (N) | Successes | UCB score per arm | Pull | Reward |
|---|---|---|---|---|---|
| 1 | 0, 0, 0 | 0, 0, 0 | untried, untried, untried | arm 1 | 0 |
| 2 | 1, 0, 0 | 0, 0, 0 | 1.177, untried, untried | arm 2 | 0 |
| 3 | 1, 1, 0 | 0, 0, 0 | 1.482, 1.482, untried | arm 3 | 0 |
| 4 | 1, 1, 1 | 0, 0, 0 | 1.665, 1.665, 1.665 | arm 1 | 0 |
| 5 | 2, 1, 1 | 0, 0, 0 | 1.269, 1.794, 1.794 | arm 2 | 1 |
| 6 | 2, 2, 1 | 0, 1, 0 | 1.339, 1.839, 1.893 | arm 3 | 1 |
| 7 | 2, 2, 2 | 0, 1, 1 | 1.395, 1.895, 1.895 | arm 2 | 0 |
At t = 4 all three arms have one pull and no success, so their scores tie and the lowest index wins. At t = 5 arm 1 has two pulls, so its bonus is smaller; arm 2 wins the tie with arm 3 and pays 1. At t = 6 arm 2's mean is 0.5, yet arm 3 still wins (1.893 against 1.839): its single pull gives it a bigger bonus, which outweighs arm 2's success. Early on, UCB1 is driven by the bonus, not the means. On the same seed, Thompson sampling's 1000 pulls ended at 16, 18, 966 per arm, with posteriors Beta(4, 14), Beta(6, 14), Beta(535, 433). The best arm's posterior is sharp, and the two bad arms were tried just enough to be confidently ruled out.
Implementation
Streaming a classifier with partial_fit, scoring each batch before training on it, and a constant step so it keeps
adapting; then Thompson sampling for a Bernoulli bandit in plain Python:
import numpy as np, random
from sklearn.linear_model import SGDClassifier
clf = SGDClassifier(loss="log_loss", learning_rate="constant", eta0=0.01, alpha=1e-4)
classes = np.array([0, 1])
errors = []
for i, (X_batch, y_batch) in enumerate(stream_of_batches()): # never holds the full data
if i > 0:
errors.append(1 - clf.score(X_batch, y_batch)) # prequential: test, then train
clf.partial_fit(X_batch, y_batch, classes=classes if i == 0 else None)
def thompson(pull, n_arms, rounds):
wins, losses = [0] * n_arms, [0] * n_arms
for _ in range(rounds):
draws = [random.betavariate(1 + wins[a], 1 + losses[a]) for a in range(n_arms)]
a = max(range(n_arms), key=draws.__getitem__)
if pull(a): # 1 if the user clicked
wins[a] += 1
else:
losses[a] += 1
return wins, losses
Other scikit-learn estimators with partial_fit include MultinomialNB, MiniBatchKMeans,
IncrementalPCA and MLPClassifier. Preprocessing must be online too: a StandardScaler fitted
on the first day leaks a stale mean into every later prediction, so update it with its own partial_fit or use
stateless features such as hashing. In PyTorch, online learning is just a training loop that calls
optimizer.step() once per arriving batch and never loops over epochs (see the
training loop).
Tradeoffs
- Online versus periodic retraining. Online updates react within seconds and use constant memory, but a bad batch (a bot attack, a logging bug) goes straight into the weights. Nightly retraining on a window is easier to test, roll back and reproduce. Many systems retrain periodically and update only a few parameters (a bias term, a bandit) online.
- Step size is the forgetting rate. Decaying schedules (sklearn's defaults) suit stationary data; under drift, use a constant step, a sliding window or a drift detector that resets.
- Catastrophic forgetting. A neural net trained online on a changed distribution can lose old skills entirely; replay buffers or regularizing toward old weights reduce it.
- Delayed and biased labels. Fraud labels arrive weeks later, and a model that blocks transactions never sees whether the blocked ones were fraud. Bandit-style exploration or holdout traffic is the only fix for the second.
- Bandits versus A/B tests. A bandit earns more during the experiment by shifting traffic to the winner; an A/B test with fixed allocation gives cleaner, unbiased effect estimates. Logged bandit data needs propensity weighting to evaluate a new policy (see causal inference).
- Policy choice. Thompson sampling is usually the best default for Bernoulli rewards and is easy to extend; UCB gives deterministic, analyzable behaviour; ε-greedy is the easiest to explain but never stops paying for exploration unless ε decays.
Why does the 1/t learner fail after the drift even though it converged nicely before?
Its step at round 200 is 0.5/200 = 0.0025, so each new example moves the weight very little. Effectively it weights all 200 old examples equally with the new ones.
An arm has been pulled 4 times with 1 success at round t = 20. What is its UCB1 score?
0.25 + √(2 ln 20 / 4) = 0.25 + √(1.498) ≈ 0.25 + 1.224 = 1.474.
Why must SGDClassifier.partial_fit be given classes on its first call?
The first batch may not contain every label, and the estimator has to allocate one weight vector per class (or one for binary) before it can train. Later batches are checked against those classes.
Connections
Read first
Read next
No page builds on this one yet.
References
- Zinkevich. Online Convex Programming and Generalized Infinitesimal Gradient Ascent. ICML 2003.
- Hazan. Introduction to Online Convex Optimization. 2016, revised 2019. arXiv:1909.05207
- Bottou. Large-Scale Machine Learning with Stochastic Gradient Descent. COMPSTAT 2010.
- Crammer, Dekel, Keshet, Shalev-Shwartz and Singer. Online Passive-Aggressive Algorithms. JMLR, 2006.
- Gama, Žliobaitė, Bifet, Pechenizkiy and Bouchachia. A Survey on Concept Drift Adaptation. ACM Computing Surveys, 2014.
- Lai and Robbins. Asymptotically Efficient Adaptive Allocation Rules. Advances in Applied Mathematics, 1985.
- Auer, Cesa-Bianchi and Fischer. Finite-time Analysis of the Multiarmed Bandit Problem. Machine Learning, 2002.
- Thompson. On the Likelihood that One Unknown Probability Exceeds Another in View of the Evidence of Two Samples. Biometrika, 1933.
- Russo, Van Roy, Kazerouni, Osband and Wen. A Tutorial on Thompson Sampling. 2018. arXiv:1707.02038
- Lattimore and Szepesvári. Bandit Algorithms. Cambridge University Press, 2020.
- scikit-learn 1.9.1:
SGDClassifier,SGDRegressor, step-size schedules.