Logistic Regression

Updated Started

Regression models predict a continuous quantity, and a probability is one. A probability-predicting model becomes a classifier once we add a decision rule, such as "if the probability is 50% or more, call it a cat".

Logistic regression predicts probabilities, so strictly it is a regression method, even though machine learning usually files it under classification because that is what it is mostly used for. Methods like SVMs are classifiers in the stricter sense: they predict an outcome without a probability.

In scikit-learn:

X = np.array([[1, 1], [1, 2], [1, 3], [1, 4], [1, 5]])  # first column is a constant, for the intercept
y = np.array([0, 0, 0, 1, 1])
 
model = LogisticRegression(random_state=0).fit(X, y)
model.predict_proba(X)[:, 1]  # [0.063, 0.161, 0.353, 0.608, 0.816]
model.predict(X)              # [0, 0, 0, 1, 1]

Odds

The odds of an event are the probability it happens over the probability it doesn't:

odds=p1−p\text{odds} = \frac{p}{1 - p}
EventProbabilityOdds
Heads1/21, or 1:1
Rolling 1 or 21/31/2, or 1:2
Drawing a diamond1/41/3, or 1:3

From a linear model to a probability

The outcome follows a Bernoulli distribution (a binomial with one trial): 1 with some unknown probability pp, 0 with probability 1−p1 - p. The goal is to estimate pp for any combination of the independent variables.

A linear combination of the variables,

z=β0+β1x1+⋯+βkxk,z = \beta_0 + \beta_1 x_1 + \cdots + \beta_k x_k,

can take any real value, so we need a link between the real line and the interval (0,1)(0, 1). The logit, the natural log of the odds, maps (0,1)(0, 1) onto the whole real line:

logit(p)=ln⁡(p1−p)=ln⁡(p)−ln⁡(1−p)\text{logit}(p) = \ln\left(\frac{p}{1 - p}\right) = \ln(p) - \ln(1 - p)
The logit function

That has the probabilities on the x axis; we want them as the output. So set the logit equal to zz and invert:

ln⁡(p1−p)=zp1−p=ezp=(1−p)ezp(1+ez)=ezp=ez1+ez=11+e−z\begin{aligned} \ln\left(\frac{p}{1 - p}\right) &= z \\ \frac{p}{1 - p} &= e^z \\ p &= (1 - p)e^z \\ p(1 + e^z) &= e^z \\ p &= \frac{e^z}{1 + e^z} = \frac{1}{1 + e^{-z}} \end{aligned}

This is the sigmoid or logistic function σ(z)\sigma(z). It maps any real input into the open interval (0,1)(0, 1), never quite reaching 0 or 1.

The sigmoid function

Fitting by maximum likelihood

The coefficients are usually found by maximum likelihood estimation. The likelihood L(β)L(\boldsymbol{\beta}) of a coefficient vector β\boldsymbol{\beta} is the product of the predicted probabilities of what actually happened:

L(β)=∏i=1np(xi)yi(1−p(xi))1−yiL(\boldsymbol{\beta}) = \prod_{i=1}^{n} p(\mathbf{x}_i)^{y_i} (1 - p(\mathbf{x}_i))^{1 - y_i}

where xi\mathbf{x}_i is the feature vector of observation ii, yi∈{0,1}y_i \in \{0, 1\} its outcome, and p(xi)=σ(xi⋅β)p(\mathbf{x}_i) = \sigma(\mathbf{x}_i \cdot \boldsymbol{\beta}) the predicted probability of the positive class.

A product of many small numbers underflows, so we maximise the log-likelihood instead:

log⁡L(β)=∑i=1n[yilog⁡p(xi)+(1−yi)log⁡(1−p(xi))]\log L(\boldsymbol{\beta}) = \sum_{i=1}^{n} \left[ y_i \log p(\mathbf{x}_i) + (1 - y_i) \log (1 - p(\mathbf{x}_i)) \right]

There is no closed-form solution, so we climb it iteratively. The sigmoid's derivative, σ′(z)=σ(z)(1−σ(z))\sigma'(z) = \sigma(z)(1 - \sigma(z)), makes the gradient come out neatly:

∇βlog⁡L=X⊤(y−p)\nabla_{\boldsymbol{\beta}} \log L = X^\top (\mathbf{y} - \mathbf{p})

That is, the error for each observation, weighted by its features and summed. The transpose is what makes the shapes line up: XX is n×mn \times m and the error is n×1n \times 1, so X⊤X^\top (which is m×nm \times n) times the error gives one m×1m \times 1 gradient entry per weight.

From scratch

  1. Start with all weights at zero.
  2. Compute scores XβX\boldsymbol{\beta} (one weighted sum per row) and pass them through the sigmoid.
  3. Subtract the predictions from the labels to get the error.
  4. Take a step up the gradient (gradient ascent, since we are maximising), scaled by the learning rate.
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
 
 
def predict_proba(x, weights):
    return sigmoid(np.dot(x, weights))
 
 
def logistic_regression(X, y, num_steps: int, learning_rate: float):
    weights = np.zeros(X.shape[1])
    for step in range(num_steps):
        error = y - predict_proba(X, weights)
        gradient = np.dot(X.T, error)
        weights += learning_rate * gradient
    return weights
 
 
weights = logistic_regression(X, y, num_steps=1_000_000, learning_rate=5e-5)
predict_proba(X, weights)  # [0.002, 0.024, 0.248, 0.817, 0.984]
From-scratch and scikit-learn logistic regression predictions against the labels

Both models classify all five points correctly, but ours is much more confident. That is not a bug: scikit-learn's LogisticRegression applies L2 regularisation by default (C=1.0), which shrinks the coefficients and pulls the probabilities towards 0.5. With penalty=None, or a very large C, it matches the from-scratch fit. With data this cleanly separable the unregularised optimum is at infinity, so our weights depend on how long we run.

Why not linear regression?

A linear model treats the classes as the numbers 0 and 1 and fits the best straight line through them. That line is not a probability: it goes below 0 and above 1, and a few extreme xx values tilt it badly. Logistic regression stays bounded and behaves at both extremes.

Linear and logistic regression fitted to binary data

Interpreting coefficients: the odds ratio

An odds ratio compares two odds. A loaded coin that lands heads 70% of the time has odds 0.7/0.3≈2.330.7 / 0.3 \approx 2.33; a fair coin has odds 1. So the odds ratio of heads, loaded against fair, is

0.7/0.30.5/0.5≈2.33\frac{0.7 / 0.3}{0.5 / 0.5} \approx 2.33

In logistic regression, eβje^{\beta_j} is the odds ratio for a one-unit increase in xjx_j, holding the other variables constant. Take a fictitious model of sleep apnoea against body weight, where weight has an odds ratio of 1.07:

  • Each extra pound multiplies the odds of apnoea by 1.07, a 7% increase.
  • Ten extra pounds multiply them by 1.0710≈1.971.07^{10} \approx 1.97, nearly doubling the odds.
  • This holds at any point on the weight scale. It is the odds that change by a constant factor, not the probability.

Original notebook

Linked from

Revision log

  1. Migrated from data-science-notes; fixed the likelihood notation and odds ratio wording

Tags: Statistics, Machine Learning