ml-models / Classical ML / Logistic regression intro

Logistic regression

Turn a weighted sum into a probability. This page builds logistic regression from log-odds, derives the cross-entropy loss and its gradient, fits it by Newton's method by hand, shows what the decision boundary and the regularization strength C do, extends it to many classes with softmax, and follows scikit-learn's LogisticRegression through its solvers.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The model
  5. The cross-entropy loss and its gradient
  6. Newton's method, a.k.a. IRLS
  7. The decision boundary
  8. Regularization and C
  9. Many classes: softmax regression
  10. Solvers
  11. Worked example
  12. Implementation
  13. Tradeoffs
  14. Check yourself
  15. Connections
  16. Read first
  17. Read next
  18. References

Problem

Many targets are yes-or-no: will this student pass, is this email spam, will this customer churn. Linear regression can't model them well. Its predictions run off past 0 and 1, squared error punishes a confident correct answer for being "too far" from the label, and one extreme point can tip the whole line.

What you want is a probability of the positive class, between 0 and 1, that still comes from an interpretable weighted sum of features. Logistic regression is exactly that, and it is far more than a baseline: it is the output layer of nearly every neural classifier, the model behind most click-through and credit-risk scores, and the template for the cross-entropy loss that trains language models.

Intuition

Keep the linear score z = w·x + b, but stop reading it as the answer. Read it as evidence: each feature pushes the score up or down, and z = 0 means "no evidence either way". Then squash the score into a probability with the sigmoid

σ(z) = 1 / (1 + e−z)

which maps 0 to ½, large positive scores toward 1 and large negative ones toward 0. Run the sigmoid backwards and you find what z really is: the log-odds, log(p / (1 − p)). The model is linear in log-odds, so each unit of a feature multiplies the odds of the positive class by ew, wherever you start.

Twelve students' hours studied and whether they passed: the fitted model is z = 1.424·hours − 4.630. Each extra hour multiplies the odds of passing by e1.424 ≈ 4.16, and the probability crosses ½ where z = 0, at 3.25 hours.

The fitted sigmoid over pass/fail data, and the straight line of log-odds underneath it Left: twelve students plotted at probability 0 (failed) or 1 (passed) against hours studied from 0.5 to 6. The fitted S-shaped curve p = σ(1.424·hours − 4.630) rises from 0.02 at half an hour to 0.98 at six hours and crosses one half at 3.25 hours, marked by a dashed vertical decision boundary. Right: the same model on the log-odds scale is a straight line through zero at 3.25 hours, from -3.92 at half an hour to 3.92 at six. 0 0.5 1 0 2 4 6 boundary 3.25 h p = σ(z) hours studied (● passed, ○ failed) -4 0 4 0 2 4 6 z = log(p / (1 − p)) z = 0 ⇔ p = ½ hours studied

Left: probabilities. Right: the same model as log-odds, where it is a plain line. The sigmoid only bends that line into the interval (0, 1).

Mechanics

The model

For an example x with d features, logistic regression predicts

p(y = 1 | x) = σ(w·x + b)    p(y = 0 | x) = 1 − σ(w·x + b) = σ(−(w·x + b))

Three sigmoid facts carry the rest of the page: σ(−z) = 1 − σ(z); its inverse is the logit, log(p / (1 − p)); and its derivative is σ′(z) = σ(z)(1 − σ(z)), largest (¼) at z = 0 and vanishing for confident predictions.

The cross-entropy loss and its gradient

Choose w and b to maximize the probability the model assigns to the labels it saw. With pi = σ(zi), the likelihood of one example is piyi(1 − pi)1−yi; take minus the log and average to get the log loss, the binary cross-entropy between the labels and the predictions:

L(w, b) = −(1/n) Σi [ yi log pi + (1 − yi) log(1 − pi) ]

A confident wrong answer costs a lot (−log 0.01 ≈ 4.6) and a confident right one almost nothing; the loss-functions page computes it straight from logits, as log(1 + e−z) for a positive label, so it never takes the log of a rounded-off zero.

Differentiate with the chain rule. For one example, ∂ℓ/∂p = −y/p + (1 − y)/(1 − p) and ∂p/∂z = p(1 − p); their product collapses to

∂ℓ/∂z = p − y    ∇wL = (1/n) Xᵀ(p − y)    ∂L/∂b = (1/n) Σ (pi − yi)

The error signal is just "prediction minus label", the same form as linear regression's residual. That is not a coincidence: both are generalized linear models with their natural link, and the same p − y appears at the output of every softmax classifier.

Setting the bias gradient to zero gives a check you can run on any unpenalized fit: the predicted probabilities sum to the number of positives. And unlike squared error, there is no closed form; ∇L = 0 is nonlinear in w. But L is convex (its Hessian, below, is positive semidefinite), so any method that reaches a stationary point has found the global minimum.

Newton's method, a.k.a. IRLS

Because the loss is smooth and convex with a cheap Hessian, Newton's method is the classic solver. Differentiate the gradient once more:

H = (1/n) Xᵀ S X,   S = diag(pi(1 − pi))    w ← w − H⁻¹∇L

Each step solves a weighted least-squares problem, with weight pi(1 − pi) on example i, which is why statisticians call it iteratively reweighted least squares. Examples the model is already sure about get almost no weight; the uncertain ones near the boundary drive the update. Near the optimum Newton converges quadratically: the number of correct digits roughly doubles per step.

Gradient descent uses only ∇L. It is cheaper per step (no d × d solve) but its speed depends on the conditioning, just as on the linear-regression page, and uncentered features make it slow.

Newton's method against gradient descent on the pass/fail data Excess loss above the minimum, on a log scale from 1 down to 10 to the −15, against the iteration number, also on a log scale from 1 to 2000. Newton's method, starting from zero weights, has excess loss 3.4e-02 after one step, 3.1e-03 after two, 5.3e-05 after three and 2.1e-08 after four: the curve plunges. Gradient descent with learning rate 0.3 starts at 0.27, is still at 0.044 after 100 steps, and reaches 3.1e-08 only after 2000. 1 1e-5 1e-10 1e-15 1 10 100 1000 Newton / IRLS gradient descent, η = 0.3 iteration (log scale); vertical: loss − minimum

Four Newton steps beat two thousand gradient steps. Each Newton step costs a d × d solve, which is trivial here and prohibitive with a million features; that tradeoff is why scikit-learn defaults to L-BFGS, which approximates the Hessian from recent gradients.

The decision boundary

Predict the positive class when p ≥ ½, that is when w·x + b ≥ 0. The set w·x + b = 0 is a line in two dimensions and a hyperplane in d, perpendicular to w. So logistic regression is a linear classifier: its contours of equal probability are parallel hyperplanes, and ‖w‖ sets how quickly probability changes as you cross them. The p = 0.9 contour sits where w·x + b = log 9 ≈ 2.2, a distance 2.2 / ‖w‖ from the boundary.

The threshold ½ is a choice, not part of the model. If a missed positive costs ten times a false alarm, predict positive above p = 1/11; the probabilities are what you threshold, which only makes sense if they are calibrated. Unpenalized logistic regression tends to be well calibrated on the data it was fitted to; strong penalties pull probabilities toward ½.

Regularization and C

If the classes are linearly separable, the unpenalized loss has no minimum. Any separating w can be doubled to push every pi closer to its label and the loss closer to zero, so ‖w‖ grows forever and the probabilities become 0s and 1s. With many features, separability is the norm, not the exception. A penalty fixes it.

scikit-learn writes the penalty through C, an inverse strength, in the SVM style:

minw,b   C Σi ℓ(yi, w·xi + b) + ½‖w‖²

Small C means strong regularization, small weights and soft probabilities; large C trusts the data. The default is C = 1, so LogisticRegression() is penalized unless you say otherwise (pass C=np.inf for the plain maximum-likelihood fit). The intercept is not penalized. Because the data term is a sum, the same C is effectively weaker penalty on a bigger dataset. An L1 penalty (l1_ratio=1, with the liblinear or saga solver) zeroes coefficients exactly, as lasso does. As with ridge, scale the features first, or the penalty falls unevenly on them.

Decision boundaries for three values of C, and weights that diverge without a penalty Left: twelve points with two features, six of class 0 and six of class 1, linearly separable. Three fitted decision boundaries: C = 0.05 gives weights (0.2, 0.25) with norm 0.32; C = 1 gives (0.71, 1.12) with norm 1.32, drawn with dashed p = 0.1 and p = 0.9 contours either side; C = 100 gives (3.8, 7.71) with norm 8.60, a boundary that threads the gap between the classes. Right: with no penalty, the weight norm after each of 12 Newton steps: 0.0, 0.9, 1.5, 2.3, 3.2, 5.1, 12.3, 23.6, 33.2, 42.4, 51.4, 60.4, 69.3, growing without bound while the loss falls toward zero. 0 0 3 3 6 6 C = 0.05 C = 1 C = 100 feature 1 (● class 1, ○ class 0) 0 35 70 0 6 12 no penalty: ‖w‖ per Newton step loss → 1e-04 Newton step

Left: only C = 100 separates every training point. The stronger penalties give up the class 1 point at (3, 3.5), predicting 0.48 and 0.42 for it, in exchange for a much smaller ‖w‖, so their probability ramps gently (the dashed contours for C = 1 sit far apart). Right: without a penalty the separable data drives the weights off to infinity.

Many classes: softmax regression

With K classes, give each class its own weight vector and score, zk = wk·x + bk, and turn the scores into a distribution with the softmax:

p(y = k | x) = ezk / Σj ezj    ℓ = −log p(y | x)    ∂ℓ/∂zk = pk − [k = y]

The loss is categorical cross-entropy and the gradient is again "prediction minus one-hot label". With K = 2 it reduces to the sigmoid on z1 − z0: adding the same number to every score changes nothing, so only differences matter. This is also how a neural classifier ends: an nn.Linear layer producing K logits, then CrossEntropyLoss. In matrix form the scores are XWᵀ + b with W of shape (K, d), which is exactly coef_'s shape in scikit-learn.

The alternative is one-vs-rest: K separate binary models, each "class k versus everything else", with probabilities renormalized afterwards. Since version 1.5 scikit-learn's LogisticRegression always fits the true multinomial model when K ≥ 3 (except with liblinear, which is binary only); wrap it in OneVsRestClassifier to get one-vs-rest.

Solvers

All of scikit-learn's solvers minimize the same objective; they differ in how they get there and which penalties they support.

  • lbfgs (default): quasi-Newton, builds a Hessian approximation from the last few gradients. A good general choice for dense data and L2 or no penalty.
  • newton-cholesky: exact Newton steps, factoring the d × d Hessian. Best when n is much larger than d and d is modest (say, under a thousand), including one-hot features.
  • newton-cg: Newton steps solved approximately by conjugate gradient, using only Hessian-vector products.
  • liblinear: coordinate descent; binary only; supports L1. Unusually, it penalizes the intercept too, so set intercept_scaling high if that matters.
  • sag and saga: stochastic average gradient, for very large n; saga also handles L1 and elastic net. Both need standardized features to converge quickly.
TermShape/ParamsMeaningSource
LogisticRegression C, l1_ratio, solver, max_iter Regularized logistic and multinomial regression; stores coef_ (1 or K, d) and intercept_. _logistic.py
C, l1_ratio, solver C > 0, default 1.0; l1_ratio in [0, 1] Inverse penalty strength, L1/L2 mix, and the solver-penalty compatibility table. LogisticRegression docstring
LogisticRegression.fit X (n, d), y (n,) Validates input, encodes classes, picks binary or multinomial, dispatches to the path solver. fit
_logistic_regression_path Cs, solver, coef Builds the loss (HalfBinomialLoss or HalfMultinomialLoss) and runs the solver for each C. _logistic_regression_path
lbfgs branch l2_reg_strength = 1 / (C · n) Rescales C into a per-sample penalty and calls scipy.optimize.minimize(method="L-BFGS-B"). _logistic.py
LinearModelLoss coef (d + 1,) or (K, d + 1) Mean loss plus ½·strength·‖w‖² (intercept excluded), with gradients and Hessians. _linear_loss.py
gradient_hessian H (d + 1, d + 1) Xᵀ S X with S = diag(p(1 − p)) plus the penalty: the IRLS Hessian. gradient_hessian
NewtonCholeskySolver tol, max_iter Newton steps by Cholesky factorization of the Hessian, with a line search and an L-BFGS fallback. _newton_solver.py
decision_function X (n, d) → (n,) or (n, K) The raw scores Xwᵀ + b: log-odds in the binary case. _base.py
predict X (n, d) → (n,) Thresholds the score at 0 (binary) or takes the argmax (multiclass). _base.py
predict_proba X (n, d) → (n, K) Sigmoid of the score (binary) or softmax over the K scores (multinomial). predict_proba, _predict_proba_lr
nn.Linear weight (K, d), bias (K,) The same model as a layer: produces the logits. linear.py
BCEWithLogitsLoss, CrossEntropyLoss logits (N,) or (N, K), targets The binary and multinomial log losses, computed stably from logits. BCEWithLogitsLoss, CrossEntropyLoss

Worked example

Newton's method by hand. Twelve students studied 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0 hours; passes are 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1. Start at w = b = 0. Every pi is ½, the loss is log 2 ≈ 0.693, and every Hessian weight p(1 − p) is ¼.

  • Gradient (as sums): Σ(p − y)x = ½·39 − 27 = -7.5, and Σ(p − y) = 6 − 6 = 0.
  • Hessian: ¼·[[Σx², Σx], [Σx, n]] = ¼·[[162.5, 39], [39, 12]].
  • Solving H·step = gradient and subtracting gives (w, b) = (0.839, -2.727), loss 0.399.

Repeating, with p and the weights recomputed each time:

stepwbmean log loss‖∇‖
00.00000.00000.6931477.5e+00
10.8392-2.72730.3989401.6e+00
21.2275-3.98920.3679664.0e-01
31.3975-4.54200.3649184.8e-02
41.4240-4.62790.3648659.4e-04
51.4245-4.62960.3648653.9e-07

The gradient norm goes 1.6, 0.4, 0.05, 0.0009, 4 × 10⁻⁷: quadratic convergence. Read the answer: the boundary is at −b/w = 3.25 hours (the data are symmetric about it), each hour multiplies the odds of passing by 4.16, and a student who studied 2 hours has p = σ(1.424·2 − 4.630) = 0.14 while one who studied 4 has 0.74. As promised, the fitted probabilities (0.02, 0.04, 0.08, 0.14, 0.26, 0.41, 0.59, 0.74, 0.86, 0.92, 0.96, 0.98) sum to 6.0000, the number of passes.

Separable data and C. On the twelve two-feature points above, C = 0.05 gives w = (0.2, 0.25), b = -1.571; C = 1 gives w = (0.71, 1.117), b = -6.370; C = 100 gives w = (3.802, 7.711), b = -37.916. The two stronger penalties misclassify the class 1 point (3, 3.5), at p = 0.48 and 0.42, because fitting it would need larger weights; C = 100 gets it right at 0.62. The direction of w changes only a little; its length grows from 0.32 to 8.60. The borderline point (4.5, 2.5), a class 0 point sitting among class 1's, gets p = 0.49, 0.41 and 0.18: larger C makes the model more certain. With no penalty, Newton never stops: ‖w‖ reaches 69 after 12 steps and the loss 1e-04, on its way to zero.

Softmax, one example. Scores for cat, dog, bird are 2.0, 1.0, -1.0; the true class is cat. Exponentiate: e² ≈ 7.389, e¹ ≈ 2.718, e⁻¹ ≈ 0.368, total 10.475. The probabilities are 0.705, 0.259, 0.035; the loss is −log 0.705 = 0.349; and the gradient with respect to the scores is p − one-hot = (-0.295, 0.259, 0.035). It pushes the cat score up and the other two down, each in proportion to how much probability it wrongly holds, and the components sum to zero.

Implementation

import torch
import torch.nn.functional as F

hours = torch.tensor([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0]).unsqueeze(1)
passed = torch.tensor([0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1], dtype=torch.float32).unsqueeze(1)

model = torch.nn.Linear(1, 1)                 # z = w * hours + b, a logit
opt = torch.optim.LBFGS(model.parameters(), line_search_fn="strong_wolfe")

def closure():
    opt.zero_grad()
    loss = F.binary_cross_entropy_with_logits(model(hours), passed)
    loss.backward()
    return loss

for _ in range(10):
    opt.step(closure)
model.weight, model.bias                      # ≈ 1.4245 and -4.6296
torch.sigmoid(model(torch.tensor([[2.0], [4.0]])))   # ≈ 0.14 and 0.74

# K classes: K logits, then cross-entropy on the integer label
logits = torch.tensor([[2.0, 1.0, -1.0]])
F.cross_entropy(logits, torch.tensor([0]))    # 0.3490
import numpy as np
from sklearn.linear_model import LogisticRegression

X = np.array([0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6]).reshape(-1, 1)
y = np.array([0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1])

clf = LogisticRegression(C=np.inf).fit(X, y)  # C=inf: no penalty
clf.coef_, clf.intercept_                     # ≈ [[1.4245]], [-4.6296]
clf.predict_proba([[2.0], [4.0]])[:, 1]       # ≈ [0.14, 0.74]
clf.decision_function([[3.25]])               # ≈ [0.0], the boundary

LogisticRegression().fit(X, y).coef_          # C=1 by default: a smaller slope

Newton's method in plain Python:

import math

def sigmoid(z):
    return 1 / (1 + math.exp(-z)) if z >= 0 else math.exp(z) / (1 + math.exp(z))

def fit_logistic_1d(xs, ys, steps=8):
    w = b = 0.0
    for _ in range(steps):
        ps = [sigmoid(w * x + b) for x in xs]
        gw = sum((p - y) * x for p, x, y in zip(ps, xs, ys))
        gb = sum(p - y for p, y in zip(ps, ys))
        s = [p * (1 - p) for p in ps]
        hww = sum(si * x * x for si, x in zip(s, xs))
        hwb = sum(si * x for si, x in zip(s, xs))
        hbb = sum(s)
        det = hww * hbb - hwb * hwb             # solve the 2x2 Newton system
        w -= (hbb * gw - hwb * gb) / det
        b -= (hww * gb - hwb * gw) / det
    return w, b

fit_logistic_1d([0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6],
                [0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1])   # (1.4245, -4.6296)

Tradeoffs

  • Linear boundary. The model can only separate classes with a hyperplane in its features. Interactions, polynomial terms or splines bend it, at the cost of more columns; past that point, trees or neural networks learn the features instead.
  • Interpretable, with caveats. Coefficients are log odds ratios, holding the other features fixed, and carry every caution from linear regression: units, correlated features and confounders. Penalties also shrink them, so a regularized coefficient understates the effect it estimates.
  • Probabilities. A fit by maximum likelihood gives fairly well calibrated probabilities, which most classifiers (SVMs, boosted trees) do not; check with a reliability plot before trusting them, especially under strong regularization or class imbalance.
  • Imbalance. With rare positives, the intercept absorbs the base rate and accuracy is meaningless. Use class_weight, a moved threshold, and metrics like precision, recall or log loss rather than accuracy.
  • Solver choice. Newton-type methods converge in a handful of steps but scale as d² memory and d³ time per step; first-order and stochastic methods scale to huge data but need scaled features and more iterations. A ConvergenceWarning usually means unscaled features, not too few iterations.

Check yourself

A coefficient is 0.7. What does it say about the odds?

Each unit increase in that feature, others fixed, multiplies the odds of the positive class by e0.7 ≈ 2.0. The change in probability depends on where you start; the change in odds does not.

Why is the gradient of the log loss with respect to the score just p − y?

The derivative of the loss with respect to p has a factor 1/(p(1 − p)), and the sigmoid's derivative is p(1 − p). They cancel, leaving p − y.

Your unpenalized fit reports huge coefficients and a convergence warning. What is happening?

The classes are (nearly) linearly separable, so the loss keeps falling as the weights grow and there is no finite minimum. Add a penalty (a finite C), which gives a unique, finite solution.

You raise C from 1 to 100. What happens to the weights and to the predicted probabilities?

The penalty weakens, so the weights grow and the probabilities move toward 0 and 1. Training loss drops; whether test loss does is a question for cross-validation (LogisticRegressionCV).

Connections

References

  • Hastie, Tibshirani and Friedman. The Elements of Statistical Learning, 2nd ed., section 4.4. Springer, 2009.
  • Bishop. Pattern Recognition and Machine Learning, sections 4.3.2–4.3.4 (logistic regression, IRLS, multiclass). Springer, 2006.
  • Cox. The Regression Analysis of Binary Sequences. Journal of the Royal Statistical Society B, 1958.
  • Nelder and Wedderburn. Generalized Linear Models. Journal of the Royal Statistical Society A, 1972.
  • Albert and Anderson. On the Existence of Maximum Likelihood Estimates in Logistic Regression Models. Biometrika, 1984.
  • Fan, Chang, Hsieh, Wang and Lin. LIBLINEAR: A Library for Large Linear Classification. JMLR, 2008.
  • scikit-learn 1.9.1: _logistic.py, _linear_loss.py and _newton_solver.py.
  • PyTorch v2.15.0: torch/nn/modules/linear.py and torch/nn/modules/loss.py.