Linear Classification & Logistic Regression

Master first-principles derivations of the log-odds ratio, sigmoid mapping, Maximum Likelihood cross-entropy loss, analytical gradient vector, and zero-dependency implementations in Python and Rust.

01. Supervised Machine Learning Engineering8 min read

Linear classification forms the bedrock of parametric discriminative modeling in modern machine learning. While linear regression directly predicts an unbounded real-valued response y^∈R\hat{y} \in \mathbb{R}, classification requires constraining predictions to a bounded probability interval p^∈[0,1]\hat{p} \in [0, 1].

Logistic Regression bridges this gap not by fitting non-linear curves directly, but by asserting that the log-odds of the target class is a linear combination of input features.

Convex LandscapeOptimization
100%

Binary Cross-Entropy over linear logits guarantees a unique global minimum with zero local minima traps.

1. The Log-Odds Ratio and Sigmoid Derivation#

To understand why the Sigmoid function appears ubiquitous in machine learning, we derive it directly from first principles by modeling event odds.

Architecture Diagram
Rendering diagram...

Step-by-Step Derivation of the Sigmoid Function#

Let p=P(y=1∣x)p = P(y = 1 | x) denote the conditional probability that an instance belongs to the positive class.

1

Step 1: Formulate the Odds Ratio

The odds of an event is the ratio between the probability that the event occurs to the probability that it does not occur:

Odds(p)=p1−p,where p∈(0,1)\text{Odds}(p) = \frac{p}{1 - p}, \quad \text{where } p \in (0, 1)

When p→0p \to 0, Odds→0\text{Odds} \to 0. When p→1p \to 1, Odds→∞\text{Odds} \to \infty. Thus, the odds maps probabilities from (0,1)(0, 1) onto [0,∞)[0, \infty).

2

Step 2: Formulate the Log-Odds (Logit)

To map the non-negative odds onto the entire real number line (−∞,∞)(-\infty, \infty), we take the natural logarithm:

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

Step 3: Assert the Linear Model

We model the logit as a linear combination of parameters w∈Rdw \in \mathbb{R}^d and feature vector x∈Rdx \in \mathbb{R}^d plus bias b∈Rb \in \mathbb{R}:

ln⁡(p1−p)=wTx+b=z\ln\left( \frac{p}{1 - p} \right) = w^T x + b = z
4

Step 4: Invert the Logit to Solve for Probability $p$

Exponentiate both sides of the linear relation:

p1−p=ez\frac{p}{1 - p} = e^z

Multiply both sides by (1−p)(1 - p):

p=ez(1−p)=ez−pezp = e^z(1 - p) = e^z - p e^z

Gather all terms containing pp on the left side:

p+pez=ez  ⟹  p(1+ez)=ezp + p e^z = e^z \implies p(1 + e^z) = e^z

Divide both sides by (1+ez)(1 + e^z):

p=ez1+ezp = \frac{e^z}{1 + e^z}

Multiply the numerator and denominator by e−ze^{-z}:

p=ez⋅e−z(1+ez)⋅e−z=11+e−z=σ(z)p = \frac{e^z \cdot e^{-z}}{(1 + e^z) \cdot e^{-z}} = \frac{1}{1 + e^{-z}} = \sigma(z)

This establishes that the standard Sigmoid function σ(z)=11+e−z\sigma(z) = \frac{1}{1 + e^{-z}} is the algebraic inverse of the linear log-odds model.

2. Mathematical Derivative of the Sigmoid Function#

During gradient descent, we must compute the derivative of σ(z)\sigma(z) with respect to zz. We derive this using the quotient rule:

σ(z)=11+e−z=(1+e−z)−1\sigma(z) = \frac{1}{1 + e^{-z}} = (1 + e^{-z})^{-1}

Applying the power and chain rules:

ddzσ(z)=−1⋅(1+e−z)−2⋅ddz(1+e−z)\frac{d}{dz}\sigma(z) = -1 \cdot (1 + e^{-z})^{-2} \cdot \frac{d}{dz}(1 + e^{-z})
ddz(1+e−z)=−e−z\frac{d}{dz}(1 + e^{-z}) = -e^{-z}

Substitute the internal derivative back into the expression:

ddzσ(z)=−1⋅(1+e−z)−2⋅(−e−z)=e−z(1+e−z)2\frac{d}{dz}\sigma(z) = -1 \cdot (1 + e^{-z})^{-2} \cdot (-e^{-z}) = \frac{e^{-z}}{(1 + e^{-z})^2}

Decompose the fraction into two factors:

ddzσ(z)=(11+e−z)⋅(e−z1+e−z)\frac{d}{dz}\sigma(z) = \left( \frac{1}{1 + e^{-z}} \right) \cdot \left( \frac{e^{-z}}{1 + e^{-z}} \right)

Observe that e−z1+e−z=(1+e−z)−11+e−z=1−11+e−z=1−σ(z)\frac{e^{-z}}{1 + e^{-z}} = \frac{(1 + e^{-z}) - 1}{1 + e^{-z}} = 1 - \frac{1}{1 + e^{-z}} = 1 - \sigma(z). Therefore:

ddzσ(z)=σ(z)⋅(1−σ(z))\frac{d}{dz}\sigma(z) = \sigma(z) \cdot \big(1 - \sigma(z)\big)
Computational Efficiency
Because the derivative of the Sigmoid function evaluates directly as σ(z)(1−σ(z))\sigma(z)(1 - \sigma(z)), we can cache the forward activation y^=σ(z)\hat{y} = \sigma(z) and evaluate the local gradient during backpropagation with a single subtraction and multiplication without invoking transcendental exponential routines again.

3. Deriving Binary Cross-Entropy via Maximum Likelihood#

Why do we not optimize Logistic Regression using Mean Squared Error (MSE)? Under MSE with non-linear sigmoid activations, the loss surface becomes non-convex, with saturated plateaus where gradients vanish.

Instead, we formulate the loss function using Maximum Likelihood Estimation (MLE).

Step-by-Step MLE Derivation#

Assume binary labels yi∈{0,1}y_i \in \{0, 1\} drawn independently from a Bernoulli distribution conditioned on input xix_i:

P(yi∣xi;w)=y^iyi⋅(1−y^i)1−yiP(y_i | x_i; w) = \hat{y}_i^{y_i} \cdot (1 - \hat{y}_i)^{1 - y_i}

Where y^i=σ(wTxi+b)\hat{y}_i = \sigma(w^T x_i + b).

1

Step 1: Construct the Likelihood Function

For NN independent and identically distributed (i.i.d.) observations, the joint likelihood L(w)L(w) is the product of individual probabilities:

L(w)=∏i=1NP(yi∣xi;w)=∏i=1Ny^iyi(1−y^i)1−yiL(w) = \prod_{i=1}^N P(y_i | x_i; w) = \prod_{i=1}^N \hat{y}_i^{y_i} (1 - \hat{y}_i)^{1 - y_i}
2

Step 2: Formulate the Log-Likelihood

Because products are computationally unstable and derivatives of sums are cleaner than derivatives of products, we take the natural logarithm:

ℓ(w)=ln⁡L(w)=∑i=1Nln⁡(y^iyi(1−y^i)1−yi)\ell(w) = \ln L(w) = \sum_{i=1}^N \ln\left( \hat{y}_i^{y_i} (1 - \hat{y}_i)^{1 - y_i} \right)

Apply log logarithm power rules:

ℓ(w)=∑i=1N[yiln⁡(y^i)+(1−yi)ln⁡(1−y^i)]\ell(w) = \sum_{i=1}^N \Big[ y_i \ln(\hat{y}_i) + (1 - y_i) \ln(1 - \hat{y}_i) \Big]
3

Step 3: Define the Empirical Loss Function (Binary Cross-Entropy)

In machine learning optimization, algorithms minimize an objective rather than maximize. We define the empirical cost J(w)J(w) as the negative average log-likelihood:

J(w)=−1Nℓ(w)=−1N∑i=1N[yiln⁡(y^i)+(1−yi)ln⁡(1−y^i)]J(w) = -\frac{1}{N} \ell(w) = -\frac{1}{N} \sum_{i=1}^N \Big[ y_i \ln(\hat{y}_i) + (1 - y_i) \ln(1 - \hat{y}_i) \Big]

4. Analytical Gradient Vector Derivation#

To update parameters via gradient descent:

w←w−η∇wJ(w)w \leftarrow w - \eta \nabla_w J(w)

We apply the multivariate chain rule for a single training example (xi,yi)(x_i, y_i) with linear logit zi=wTxi+bz_i = w^T x_i + b:

∂Ji∂wj=∂Ji∂y^i⋅∂y^i∂zi⋅∂zi∂wj\frac{\partial J_i}{\partial w_j} = \frac{\partial J_i}{\partial \hat{y}_i} \cdot \frac{\partial \hat{y}_i}{\partial z_i} \cdot \frac{\partial z_i}{\partial w_j}

Let us compute each partial derivative individually:

  1. Loss with respect to activation:
∂Ji∂y^i=−(yiy^i−1−yi1−y^i)=−yi(1−y^i)−(1−yi)y^iy^i(1−y^i)=−yi−y^iy^i(1−y^i)=y^i−yiy^i(1−y^i)\frac{\partial J_i}{\partial \hat{y}_i} = -\left( \frac{y_i}{\hat{y}_i} - \frac{1 - y_i}{1 - \hat{y}_i} \right) = -\frac{y_i(1 - \hat{y}_i) - (1 - y_i)\hat{y}_i}{\hat{y}_i(1 - \hat{y}_i)} = -\frac{y_i - \hat{y}_i}{\hat{y}_i(1 - \hat{y}_i)} = \frac{\hat{y}_i - y_i}{\hat{y}_i(1 - \hat{y}_i)}
  1. Activation with respect to logit:
∂y^i∂zi=y^i(1−y^i)\frac{\partial \hat{y}_i}{\partial z_i} = \hat{y}_i(1 - \hat{y}_i)
  1. Logit with respect to weight parameter:
∂zi∂wj=xi,j\frac{\partial z_i}{\partial w_j} = x_{i,j}

Multiply the three partial derivatives:

∂Ji∂wj=(y^i−yiy^i(1−y^i))⋅(y^i(1−y^i))⋅xi,j\frac{\partial J_i}{\partial w_j} = \left( \frac{\hat{y}_i - y_i}{\hat{y}_i(1 - \hat{y}_i)} \right) \cdot \Big( \hat{y}_i(1 - \hat{y}_i) \Big) \cdot x_{i,j}

Notice how the non-linear denominator y^i(1−y^i)\hat{y}_i(1 - \hat{y}_i) cancels out:

∂Ji∂wj=(y^i−yi)⋅xi,j\frac{\partial J_i}{\partial w_j} = (\hat{y}_i - y_i) \cdot x_{i,j}

Summing over all NN training samples yields the exact gradient vector:

∇wJ(w)=1N∑i=1N(y^i−yi)xi=1NXT(Y^−Y)\nabla_w J(w) = \frac{1}{N} \sum_{i=1}^N (\hat{y}_i - y_i) x_i = \frac{1}{N} X^T (\hat{Y} - Y)
∂J(w)∂b=1N∑i=1N(y^i−yi)\frac{\partial J(w)}{\partial b} = \frac{1}{N} \sum_{i=1}^N (\hat{y}_i - y_i)
Structural Symmetry with Linear Regression
The gradient vector expression 1NXT(Y^−Y)\frac{1}{N} X^T (\hat{Y} - Y) matches ordinary least squares linear regression. The sole difference lies in the hypothesis function: linear regression uses Y^=Xw\hat{Y} = Xw, while logistic regression applies the non-linear sigmoid map Y^=σ(Xw)\hat{Y} = \sigma(Xw).

5. From-Scratch Implementation: Zero Dependencies#

The following implementations demonstrate pure mathematical logistic regression in Python and Rust without relying on any external packages like NumPy, PyTorch, or ndarray.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import math from typing import List, Tuple class LogisticRegressionFromScratch: def __init__(self, learning_rate: float = 0.1, num_iterations: int = 1000): self.learning_rate = learning_rate self.num_iterations = num_iterations self.weights: List[float] = [] self.bias: float = 0.0 @staticmethod def _sigmoid(z: float) -> float: # Prevent math overflow for extreme negative logits if z < -700.0: return 0.0 if z > 700.0: return 1.0 return 1.0 / (1.0 + math.exp(-z)) def fit(self, X: List[List[float]], y: List[float]) -> None: num_samples = len(X) num_features = len(X[0]) # Initialize parameters to zeros self.weights = [0.0] * num_features self.bias = 0.0 for _ in range(self.num_iterations): # Gradient accumulators dw = [0.0] * num_features db = 0.0 for i in range(num_samples): # Compute linear score: z = w^T x + b z = sum(self.weights[j] * X[i][j] for j in range(num_features)) + self.bias y_hat = self._sigmoid(z) error = y_hat - y[i] # Accumulate gradients for j in range(num_features): dw[j] += error * X[i][j] db += error # Apply gradient descent update for j in range(num_features): self.weights[j] -= (self.learning_rate / num_samples) * dw[j] self.bias -= (self.learning_rate / num_samples) * db def predict_proba(self, X: List[List[float]]) -> List[float]: num_features = len(self.weights) probabilities = [] for row in X: z = sum(self.weights[j] * row[j] for j in range(num_features)) + self.bias probabilities.append(self._sigmoid(z)) return probabilities def predict(self, X: List[List[float]], threshold: float = 0.5) -> List[int]: return [1 if p >= threshold else 0 for p in self.predict_proba(X)] # Verification on toy separable dataset if __name__ == "__main__": X_train = [[0.5, 1.5], [1.0, 1.0], [1.5, 2.5], [3.0, 3.5], [3.5, 4.0], [4.0, 3.0]] y_train = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0] model = LogisticRegressionFromScratch(learning_rate=0.5, num_iterations=500) model.fit(X_train, y_train) preds = model.predict(X_train) print(f"Learned weights: {model.weights}") print(f"Learned bias: {model.bias:.4f}") print(f"Training predictions: {preds}")

6. Active Knowledge Checks#

Test your comprehension of the mathematical derivations and optimization landscape:

Concept check

Why does the derivative term sigma(z)(1 - sigma(z)) completely disappear from the final gradient vector during backpropagation of Logistic Regression?

Concept check

What happens to the optimization surface if one optimizes Logistic Regression using Mean Squared Error instead of Binary Cross-Entropy?

Finished reading this lesson?