SGD Classifier & Regressor, In Full Depth
Linear Regression derived gradient descent from scratch, and Logistic Regression did the same for a classifier. Both computed the gradient over the whole dataset before taking a single step. This page covers what happens when you take a step after every example instead: why that still points downhill on average, the exact update rule for each loss and penalty, how the step size has to shrink for the method to converge, and why one training loop is enough to fit linear regression, ridge, lasso, logistic regression and a linear SVM. SGDClassifier and SGDRegressor in scikit-learn are that loop with the loss and penalty exposed as settings.
You're cooking a large pot of soup and want to know if it needs salt. You could pour the whole pot into a bowl and taste it all, which is accurate but slow. Or you can taste one spoonful, adjust a little, stir, and taste another spoonful. Each spoonful is a slightly noisy sample of the whole pot, but on average it tells you the truth, and you get to adjust hundreds of times while the first cook is still pouring.
Stochastic gradient descent is the spoonful method. Instead of measuring the model's error on every example before each correction, it measures the error on one example (or a small handful), corrects a little, and moves on. Everything below is that idea made exact: why one spoonful is an honest sample, how big each correction should be, and why the corrections have to get smaller over time.
1. One Model, Many Losses
Every linear model in this section makes its prediction the same way. It computes a score
and then either reports directly (regression) or reports its sign (classification). What separates linear regression from logistic regression from a linear SVM is not the model. It's the training objective, which always has the same two-part shape:
is a loss that measures how bad the score is for the true label . is a penalty on the weights, and sets how much it counts. Choose the pair and you've chosen the model:
| Model | Loss | Penalty |
|---|---|---|
| Linear Regression | Squared error | None |
| Ridge | Squared error | L2: |
| Lasso | Squared error | L1: |
| Elastic Net | Squared error | A mix of L1 and L2 |
| Logistic Regression | Log loss | L2 (usually) |
| Linear SVM | Hinge | L2 |
(For the classification losses, the labels are written as , which makes positive exactly when the prediction has the right sign. The product is called the margin.)
Each row has its own dedicated solver elsewhere: the normal equation for linear regression, coordinate descent for lasso, L-BFGS for logistic regression, a quadratic-programming solver for SVMs. But all of them are minimizing an objective of the shape above, and one generic algorithm can minimize any of them as long as it can compute the gradient of and . The diagram below runs that one loop on the same data with three different (loss, penalty) pairs:
That's the whole idea behind SGDClassifier and SGDRegressor: not a new model, one optimizer with a menu. The limitation, which the rest of the page has to deal with, is that the optimizer is a noisy one. The next section shows where the noise comes from and why it doesn't break anything.
2. From the Full Gradient to One Example
Batch gradient descent, as derived on the Linear Regression page, updates the weights using the gradient of the full objective. Ignore the penalty for a moment and write the data term as an average of per-example losses, :
The gradient of an average is the average of the gradients. Every batch step therefore touches all examples. With ten million rows, that's ten million gradient evaluations for one small move.
The stochastic step. Pick one index uniformly at random and use that single example's gradient instead:
Here is the step size (the learning rate). This looks like a crude approximation, but take its expected value over the random choice of . Each index has probability :
The single-example gradient is an unbiased estimate of the true gradient. On any one step it may point in a poor direction, but on average it points exactly where batch gradient descent would. The cost of a step drops from gradient evaluations to one.
The price is variance. How far a single gradient strays from the average is measured by its variance, . Averaging examples drawn independently (a mini-batch) divides it by :
So the batch size is a dial between cost per step ( gradient evaluations) and noise per step (). is pure SGD; is batch gradient descent; deep learning mostly lives in between, at batch sizes of 32 to a few thousand, because a GPU computes 256 gradients in roughly the time it takes to compute one (see Optimizers: Gradient Descent Variants). scikit-learn's SGD estimators use .
Why the noise is often worth it. Early in training, almost every example agrees on the rough direction to move, so one example is nearly as informative as all of them. SGD makes updates in the time batch gradient descent makes one, and in a single pass through the data it typically gets most of the way to the minimum. Léon Bottou's analysis of large-scale learning makes the point precisely: once data is plentiful, the limiting factor is computation, not data, and an optimizer that reaches a good-enough solution cheaply beats one that reaches a perfect solution expensively, because the model's statistical error dwarfs the remaining optimization error anyway.
The unbiased estimate tells us the direction is right on average. It says nothing about how to turn a score into a gradient for each loss in the table, which is what the next section works out.
3. The Update Rule for Each Loss
Every loss in the table depends on only through the score . That gives the chain rule a fixed shape:
So the update for every model is "some scalar, times the input vector". Only the scalar changes. Work it out for each loss.
Squared error, :
The residual itself. This is the per-example version of the linear regression gradient; the factor just cancels the 2 from differentiating the square.
Log loss, . Differentiate the outer log, then the exponent:
Divide the top and bottom by :
Read the size of that number. When the margin is large and positive (confidently right), is huge and : the example barely moves the weights. When the margin is large and negative (confidently wrong), and : a full-strength correction toward the right label. Written with labels in and , the same quantity is , the form derived on the Logistic Regression page.
Hinge, . This has a corner at , where no derivative exists. Away from the corner:
At the corner itself any value between and is a valid subgradient (a slope of a line that touches the function there without crossing it), and implementations just pick one. The rule is blunt: an example on the correct side of the margin, by at least 1, contributes nothing at all. Only examples inside the margin or on the wrong side move the weights, which is the SGD view of why an SVM depends only on its support vectors.
The rest of the menu, briefly:
| Loss | What it buys | |
|---|---|---|
perceptron: | if , else | The 1958 perceptron. No margin, only fixes mistakes |
squared_hinge: | Smooth version of hinge | |
modified_huber | Quadratic near the margin, linear when badly wrong | Tolerant of outliers and still gives probability estimates |
huber (regression) | when , else | Squared error for small residuals, absolute for outliers |
epsilon_insensitive (regression) | inside a tube of width , outside | Linear SVR |
That's the full set the scikit-learn estimators accept. Each one plugs into the same two lines, and . The penalty term is what's still missing.
4. The Penalty Inside the Update
Add the penalty back. The per-example objective SGD actually samples is , so each step subtracts the penalty's gradient too.
L2. With , the gradient is , and the step becomes
Before each data-driven correction, every weight is multiplied by a number slightly less than one. That's why the L2 penalty is also called weight decay: left alone, the weights shrink geometrically toward zero, and only consistent pressure from the data keeps a weight large. The bias is normally left out of the penalty, since shrinking the intercept toward zero would just bias predictions toward zero.
L1. With , the subgradient is , a constant-size pull toward zero for each weight. The naive update has a problem that matters a lot in practice: a weight that should be exactly zero almost never lands exactly on zero. It overshoots back and forth across zero by about every step, so you lose the sparsity that was the reason for choosing L1 (see Lasso: Why L1 Produces Exact Zeros).
The fix is to clip instead of subtract: after the data step, move each weight toward zero by the penalty amount, but stop at zero rather than crossing it:
This is the soft-thresholding operator, the same one that appears in lasso's coordinate descent. Tsuruoka, Tsujii and Ananiadou (2009) refined it further by tracking the total penalty each weight should have received so far and applying the difference, which handles weights that sit at zero for many steps and then receive a gradient. That cumulative-penalty method is what scikit-learn uses for penalty='l1' and 'elasticnet'.
Elastic net applies both: a fraction l1_ratio of as the clipped L1 step and the rest as weight decay. scikit-learn's default l1_ratio is 0.15, applied only when penalty='elasticnet'.
With loss and penalty both in the update, the loop is complete except for one number: . Choosing it is where SGD differs most from batch gradient descent.
5. The Step Size Has to Shrink
With batch gradient descent and a small enough fixed learning rate, the gradient goes to zero at the minimum and so do the steps; the algorithm stops on its own. SGD doesn't get that. At the minimum the average gradient is zero, but each individual example's gradient is not; the examples disagree with each other, which is what the variance measures. With a constant the weights keep getting kicked around forever, settling into a cloud whose size grows with .
To converge to the exact minimum, the step size has to decay. Robbins and Monro (1951) gave the conditions a schedule needs:
The first says the steps can't shrink so fast that the total distance traveled is finite; otherwise SGD could stall before reaching the minimum. The second says they must shrink fast enough that the accumulated noise (which scales with ) stays bounded. satisfies both, since the harmonic series diverges while . A constant step fails the second condition; fails the first.
For a strongly convex objective (L2-penalized squared error or log loss, with ), the rate is also the best possible order: with , the expected gap shrinks like . That's the theory behind the schedules scikit-learn exposes through learning_rate:
learning_rate | Schedule | Default for |
|---|---|---|
'optimal' | , with chosen by a heuristic due to Bottou | SGDClassifier |
'invscaling' | , power_t | SGDRegressor (, ) |
'constant' | ||
'adaptive' | until the loss stops improving, then divide by 5 |
'optimal' ties the step size to , which is why changing alpha on an SGDClassifier changes both the regularization and the speed of learning. The offset keeps the first steps from being enormous (with the first step would be at the default ). SGDClassifier's default eta0 is 0, so the 'constant', 'invscaling' and 'adaptive' schedules require you to set eta0 yourself.
A good schedule handles the noise. It doesn't handle a different problem: features on very different scales make the right step size different for every weight, and a single can't be right for all of them.
6. Why Feature Scaling Is Not Optional
Look at the update again: weight moves by . If feature 1 is measured in metres (values around 1) and feature 2 in millimetres (values around 1000), weight 2 receives updates a thousand times larger for the same error , while the weight it needs is a thousand times smaller. Any small enough to keep weight 2 stable is far too small for weight 1 to learn anything.
Geometrically, the loss surface becomes a long, thin valley. Its curvature along one direction is about times its curvature along the other (curvature scales with the feature's variance, ). The usable step size is limited by the steepest direction, and progress along the shallow direction is correspondingly slow. Batch methods with second-order information (L-BFGS, Newton) partly correct for this; plain SGD has no defence against it.
The fix is to standardize each feature to mean 0 and variance 1 before training, so every weight sees updates of comparable size. On the dataset in Section 8, multiplying one feature by 1000 drops SGDClassifier's test accuracy from 0.913 to 0.807, and standardizing restores it. scikit-learn's own documentation for SGD puts this first in its practical tips, and recommends putting the scaler in a Pipeline so it's fit only on training data (fitting it on everything is a form of data leakage).
Scaling also matters for the penalty. An L2 penalty treats all weights alike, so if one feature's natural weight is a thousand times smaller than another's because of units, the penalty is effectively regularizing them very differently. After standardization, "large weight" means the same thing for every feature.
With scaling, a schedule and the loss-penalty menu, SGD is a complete batch training method. Its most distinctive use, though, is the case where there is no batch.
7. Online and Out-of-Core Learning
Nothing in the update rule requires the dataset to exist all at once. Each step needs one example, and the example can be discarded afterwards. That makes two things possible that the closed-form and batch solvers in this section can't do.
Out-of-core learning. If the training set is 500 GB on disk, read it in chunks that fit in memory, update on each chunk, and move on. Memory use is the size of one chunk plus one weight vector, regardless of how big the data is. In scikit-learn this is partial_fit, which runs a single pass over the data you pass in and keeps the model's state between calls.
Online learning. If data arrives continuously (clicks, transactions, sensor readings), keep calling partial_fit as it arrives. The model updates in seconds instead of being retrained overnight, and if the world drifts, a constant or slowly decaying step size lets it follow. This is a deliberate trade: the Robbins–Monro conditions in Section 5 assume a fixed distribution to converge to. When the distribution moves, you want the step size to stay above zero, so the model keeps forgetting old data at a controlled rate. Large-scale click-through-rate systems were long built exactly this way: logistic regression on hashed sparse features, updated online (McMahan et al. 2013 describe Google's version, which uses an adaptive per-coordinate step size called FTRL-Proximal).
Three practical details matter with partial_fit:
- Pass
classeson the first call. A chunk may not contain every label, so the classifier has to be told the full set up front. - The scaler has to be online too, or fit on an early representative sample.
StandardScaleralso has apartial_fit. - Shuffle when you can. SGD's unbiasedness argument in Section 2 assumes examples arrive in random order. If a stream arrives sorted (all of Monday, then all of Tuesday), each stretch of updates pulls the weights toward that stretch. For a fixed dataset,
fitreshuffles every epoch by default (shuffle=True).
8. Minimal Implementation
Here is the whole loop in plain NumPy: one function per loss's scalar gradient from Section 3, L2 weight decay from Section 4, and a decaying step size from Section 5. Labels are in for classification.
Output:
The same function trains a logistic regression, a linear SVM and a linear regression; only the string changes. The two classifiers find boundaries pointing in the same direction (the ratio of the two weights is about 1.44 in both), but at different scales: log loss keeps pushing confident examples further, so its weights keep growing until the penalty balances them, while hinge stops caring once every example clears the margin. The regression recovers the true coefficients to within the noise.
The library versions add the rest of this page: every loss, L1 and elastic-net penalties, the schedules in Section 5, averaging and early stopping. This second script uses them to check two claims made above, the effect of scaling (Section 6) and online learning with partial_fit (Section 7):
Output (scikit-learn 1.7):
Standardized SGD matches the dedicated LogisticRegression solver; the same model on unscaled features loses ten points of accuracy. The online model, which never held more than 1,000 rows in memory, gets within half a point of both.
9. When Not to Use SGD
SGD is the right tool when the data is large or arriving continuously. On small and medium problems it adds tuning (step size, schedule, epochs, scaling) that a dedicated solver doesn't need.
| Situation | Better choice |
|---|---|
| Fewer than about 100,000 rows that fit in memory | The model's own solver: LinearRegression, Ridge, Lasso, LogisticRegression (L-BFGS), LinearSVC. Exact or near-exact, almost no tuning |
| You need the precise optimum, e.g. for reporting coefficients | A batch solver. SGD's answer is only as good as its schedule and stopping rule |
| Lasso path or cross-validated on modest data | LassoCV / ElasticNetCV with coordinate descent and warm starts |
| The relationship isn't linear | Trees and boosting (XGBoost, LightGBM, CatBoost), or a neural network |
| Features can't be scaled sensibly (heavy-tailed, mixed units) and you won't preprocess | Tree models, which ignore feature scale |
| Ill-conditioned problems where plain SGD crawls | Adaptive or momentum methods (see Optimizers), or second-order batch methods |
Remember
| Idea | In one line |
|---|---|
| The framing | One linear score, a loss and a penalty; pick the pair and you've picked the model |
| Why one example works | : the single-example gradient is unbiased |
| The cost | Variance per step, divided by for a mini-batch of size |
| Every update | , with the only loss-specific part |
| Hinge vs. log loss | Hinge ignores examples beyond the margin; log loss never quite lets go |
| L2 penalty | Weight decay: multiply by every step |
| L1 penalty | Clip toward zero, don't cross it, or the sparsity is lost |
| Step size | Must decay (, ) to converge; is the classic choice |
| Averaging | average=True reports the mean iterate; robust to the step size |
| Scaling | Standardize features, inside a pipeline, every time |
| Online | partial_fit on chunks; pass classes the first time |
Next: Survival Analysis for modelling time until an event, or Deep Learning, where the same score-loss-gradient loop is stacked into many layers and mini-batch SGD with momentum and adaptive steps becomes the default optimizer.
Sources
- H. Robbins and S. Monro, A Stochastic Approximation Method, Annals of Mathematical Statistics, 1951
- L. Bottou, Large-Scale Machine Learning with Stochastic Gradient Descent, COMPSTAT 2010
- L. Bottou, F. E. Curtis and J. Nocedal, Optimization Methods for Large-Scale Machine Learning, SIAM Review, 2018
- B. T. Polyak and A. B. Juditsky, Acceleration of Stochastic Approximation by Averaging, SIAM Journal on Control and Optimization, 1992
- Y. Tsuruoka, J. Tsujii and S. Ananiadou, Stochastic Gradient Descent Training for L1-regularized Log-linear Models with Cumulative Penalty, ACL 2009
- H. B. McMahan et al., Ad Click Prediction: a View from the Trenches, KDD 2013
- scikit-learn, Stochastic Gradient Descent user guide and
SGDClassifierreference