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.
Linear classification forms the bedrock of parametric discriminative modeling in modern machine learning. While linear regression directly predicts an unbounded real-valued response , classification requires constraining predictions to a bounded probability interval .
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.
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.
Step-by-Step Derivation of the Sigmoid Function#
Let denote the conditional probability that an instance belongs to the positive class.
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:
When , . When , . Thus, the odds maps probabilities from onto .
Step 2: Formulate the Log-Odds (Logit)
To map the non-negative odds onto the entire real number line , we take the natural logarithm:
Step 3: Assert the Linear Model
We model the logit as a linear combination of parameters and feature vector plus bias :
Step 4: Invert the Logit to Solve for Probability $p$
Exponentiate both sides of the linear relation:
Multiply both sides by :
Gather all terms containing on the left side:
Divide both sides by :
Multiply the numerator and denominator by :
This establishes that the standard Sigmoid function 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 with respect to . We derive this using the quotient rule:
Applying the power and chain rules:
Substitute the internal derivative back into the expression:
Decompose the fraction into two factors:
Observe that . Therefore:
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 drawn independently from a Bernoulli distribution conditioned on input :
Where .
Step 1: Construct the Likelihood Function
For independent and identically distributed (i.i.d.) observations, the joint likelihood is the product of individual probabilities:
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:
Apply log logarithm power rules:
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 as the negative average log-likelihood:
4. Analytical Gradient Vector Derivation#
To update parameters via gradient descent:
We apply the multivariate chain rule for a single training example with linear logit :
Let us compute each partial derivative individually:
- Loss with respect to activation:
- Activation with respect to logit:
- Logit with respect to weight parameter:
Multiply the three partial derivatives:
Notice how the non-linear denominator cancels out:
Summing over all training samples yields the exact gradient vector:
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.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970import 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:
Why does the derivative term sigma(z)(1 - sigma(z)) completely disappear from the final gradient vector during backpropagation of Logistic Regression?
What happens to the optimization surface if one optimizes Logistic Regression using Mean Squared Error instead of Binary Cross-Entropy?