Logistic Regression: Theory, Mathematics, and Practical Applications
Raj Shaikh 15 min read 3141 wordsImagine you’re a wizard trying to classify magical creatures into two groups: “Friendly” and “Hostile,” based on their attributes (size, color, sound, etc.). But here’s the catch: the creatures don’t come with labels, and you need a spell (a.k.a. Logistic Regression) to help you predict which group each creature belongs to based on its features.
Logistic Regression is like the magic wand of statistics and machine learning. It’s one of the simplest yet most powerful tools for classification tasks. Unlike Linear Regression, which predicts continuous values (like “how many spells you can cast in an hour”), Logistic Regression predicts probabilities for binary outcomes (e.g., “Friendly” or “Hostile”).
It uses a beautiful combination of mathematics and logic to draw boundaries between classes. Let’s dive deep into the magical world of Logistic Regression and learn how this spell works step by step.
1. What is Logistic Regression?
Logistic Regression is a statistical method for predicting binary outcomes (e.g., “yes” or “no,” “spam” or “not spam”). While it has the term “regression,” don’t let it fool you! It’s primarily used for classification tasks.
Think of it this way:
- You give the model a set of features (like creature size, speed, and color).
- The model predicts the probability of each class (Friendly vs. Hostile).
- Based on a threshold (e.g., 0.5), the probability is converted into a class label.
In essence, Logistic Regression builds a linear boundary between the two classes, separating them in feature space.
Let’s first understand the magical Sigmoid Function, which forms the core of Logistic Regression.
2. Understanding the Logistic Function (Sigmoid Function)
The Logistic Function is the secret sauce of Logistic Regression. It’s defined as:
\[ \sigma(z) = \frac{1}{1 + e^{-z}} \]Here:
- \( z \) is the input (a linear combination of features and weights: \( z = w_1x_1 + w_2x_2 + \dots + w_nx_n + b \)).
- \( \sigma(z) \) squashes \( z \) into a range between 0 and 1, which can be interpreted as a probability.
Why the Sigmoid Function?
The Sigmoid function’s S-shape is perfect for mapping any real number into the range (0, 1). For example:
- When \( z \to -\infty \), \( \sigma(z) \to 0 \) (Very unlikely to belong to class 1).
- When \( z \to \infty \), \( \sigma(z) \to 1 \) (Very likely to belong to class 1).
- When \( z = 0 \), \( \sigma(z) = 0.5 \) (Equally likely to belong to either class).
Here’s how it looks visually:
graph TD A["Input: Feature Vector (X)"] --> B["Linear Combination: Z = WX + b"] B --> C["Sigmoid Function: σ(z) = 1 / (1 + e^(-z))"] C --> D["Output: Probability (P)"]
It’s like a magical filter: dump in raw numbers, and out comes a nice probability!
Let’s visualize this with some Python code:
import numpy as np
import matplotlib.pyplot as plt
# Sigmoid Function
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# Input range
z = np.linspace(-10, 10, 100)
sigma = sigmoid(z)
# Plot
plt.plot(z, sigma)
plt.title("Sigmoid Function")
plt.xlabel("z")
plt.ylabel("σ(z)")
plt.grid()
plt.show()
3. The Mathematics of Logistic Regression
Alright, brave wizards! Let’s uncover the spellbook and see how the magic of Logistic Regression works under the hood. The math behind Logistic Regression is not as intimidating as it sounds—it’s actually quite elegant.
Step 1: Linear Combination of Inputs
The first step is to combine the input features linearly, just like in Linear Regression. If you have \( n \) features \( x_1, x_2, \dots, x_n \), and corresponding weights \( w_1, w_2, \dots, w_n \), the linear combination is:
\[ z = w_1x_1 + w_2x_2 + \dots + w_nx_n + b = \mathbf{w}^\top \mathbf{x} + b \]Here:
- \( \mathbf{x} \): Feature vector.
- \( \mathbf{w} \): Weight vector.
- \( b \): Bias term (or intercept).
Think of this step as gathering all the creature attributes into one grand equation to judge their friendliness.
Step 2: Apply the Sigmoid Function
Now, take the linear combination \( z \) and pass it through the Sigmoid Function:
\[ \sigma(z) = \frac{1}{1 + e^{-z}} \]This maps \( z \) (which can range from \( -\infty \) to \( \infty \)) to a probability \( P(y=1 | \mathbf{x}) \), i.e., the likelihood that the output belongs to class 1.
Why use probabilities?
Probabilities make decisions flexible. Instead of saying “This creature is definitely hostile,” the model says, “There’s a 90% chance this creature is hostile,” which is way more informative.
Step 3: Decision Rule
The final step is to assign a class label based on the probability. For binary classification, we use a threshold, typically 0.5:
\[ \hat{y} = \begin{cases} 1 & \text{if } \sigma(z) \geq 0.5, \\ 0 & \text{otherwise.} \end{cases} \]In plain terms:
- If the probability is 50% or higher, the creature is “Friendly” (class 1).
- Otherwise, it’s “Hostile” (class 0).
Example Walkthrough (With Numbers)
Let’s say you’re trying to classify a creature based on two features:
- Size (\( x_1 = 3 \))
- Speed (\( x_2 = 5 \))
And your learned weights are:
- \( w_1 = 0.8 \), \( w_2 = -0.6 \), \( b = 0.5 \).
Step 1: Compute \( z \):
\[ z = w_1x_1 + w_2x_2 + b = (0.8)(3) + (-0.6)(5) + 0.5 = 2.4 - 3 + 0.5 = -0.1 \]Step 2: Apply the Sigmoid Function:
\[ \sigma(z) = \frac{1}{1 + e^{0.1}} \approx 0.475 \]Step 3: Decision Rule:
Since \( \sigma(z) = 0.475 < 0.5 \), the predicted class is \( 0 \) (Hostile).
4. Maximum Likelihood Estimation (The Core Spell)
Now that we’ve seen the mechanics of Logistic Regression, let’s look at how the model learns the weights \( \mathbf{w} \) and bias \( b \). This is where the magic of Maximum Likelihood Estimation (MLE) comes into play.
Likelihood Function
For a binary classification problem:
- \( P(y=1 | \mathbf{x}) = \sigma(z) \)
- \( P(y=0 | \mathbf{x}) = 1 - \sigma(z) \)
The likelihood of observing the actual labels \( y \) for all training samples is:
\[ L(\mathbf{w}, b) = \prod_{i=1}^m \sigma(z^{(i)})^{y^{(i)}} \cdot (1 - \sigma(z^{(i)}))^{1 - y^{(i)}} \]Here:
- \( m \): Number of training samples.
- \( z^{(i)} = \mathbf{w}^\top \mathbf{x}^{(i)} + b \): Linear combination for the \( i \)-th sample.
We aim to maximize this likelihood function to find the best weights \( \mathbf{w} \) and bias \( b \).
Log-Likelihood Function
Working with the product of probabilities can get messy. So, we take the logarithm of the likelihood (logarithms turn products into sums):
\[ \ell(\mathbf{w}, b) = \sum_{i=1}^m \left[ y^{(i)} \log \sigma(z^{(i)}) + (1 - y^{(i)}) \log (1 - \sigma(z^{(i)})) \right] \]This is the Log-Likelihood Function. Maximizing it gives us the optimal parameters.
5. Decision Boundary and Its Interpretation
The Decision Boundary is like drawing a magical line on a map to separate creatures into “Friendly” and “Hostile” zones. It’s a geometric representation of the model’s predictions.
What is the Decision Boundary?
The decision boundary is the set of points where the predicted probability \( \sigma(z) \) equals the threshold (usually 0.5). At this threshold, \( z \) equals 0 (since \( \sigma(0) = 0.5 \)).
For Logistic Regression, the decision boundary is derived from:
\[ z = \mathbf{w}^\top \mathbf{x} + b = 0 \]In 2D (two features), this represents a straight line:
\[ w_1x_1 + w_2x_2 + b = 0 \]Where:
- \( w_1 \) and \( w_2 \) are the weights for features \( x_1 \) and \( x_2 \).
- \( b \) is the bias term.
This line splits the feature space into two regions:
- One side predicts \( \hat{y} = 1 \) (“Friendly”).
- The other side predicts \( \hat{y} = 0 \) (“Hostile”).
Visualizing the Decision Boundary
Let’s imagine a two-feature example:
- \( x_1 \): Size.
- \( x_2 \): Speed.
If your model learns weights \( w_1 = 0.8 \), \( w_2 = -0.6 \), and \( b = 0.5 \), the decision boundary equation is:
\[ 0.8x_1 - 0.6x_2 + 0.5 = 0 \]Rearranging for \( x_2 \):
\[ x_2 = \frac{0.8x_1 + 0.5}{0.6} \]Here’s what the decision boundary might look like in Python:
import numpy as np
import matplotlib.pyplot as plt
# Feature range
x1 = np.linspace(0, 10, 100)
# Decision boundary equation
w1, w2, b = 0.8, -0.6, 0.5
x2 = (w1 * x1 + b) / -w2
# Plot
plt.plot(x1, x2, label="Decision Boundary", color="red")
plt.fill_between(x1, x2, 10, alpha=0.2, label="Predicted Class 1 (Friendly)", color="blue")
plt.fill_between(x1, x2, 0, alpha=0.2, label="Predicted Class 0 (Hostile)", color="green")
plt.xlabel("Feature 1 (Size)")
plt.ylabel("Feature 2 (Speed)")
plt.title("Logistic Regression Decision Boundary")
plt.legend()
plt.grid()
plt.show()
6. Cost Function for Logistic Regression
Now, let’s talk about how we make Logistic Regression learn. Remember, the goal is to find weights \( \mathbf{w} \) and bias \( b \) that minimize the error in predictions. For this, we need a Cost Function.
Why Not Use Mean Squared Error (MSE)?
In Linear Regression, we used Mean Squared Error (MSE). However, MSE isn’t a good choice for Logistic Regression because:
- The Sigmoid Function causes the loss to be non-convex, making optimization challenging.
- It doesn’t align well with the probabilistic interpretation of Logistic Regression.
The Log Loss (Cross-Entropy Loss)
Instead, we use the Log Loss (also called Cross-Entropy Loss), which comes from the Log-Likelihood Function:
\[ J(\mathbf{w}, b) = -\frac{1}{m} \sum_{i=1}^m \left[ y^{(i)} \log \sigma(z^{(i)}) + (1 - y^{(i)}) \log (1 - \sigma(z^{(i)}) \right] \]Here:
- \( m \): Number of training samples.
- \( z^{(i)} \): Linear combination for the \( i \)-th sample.
- \( \sigma(z^{(i)}) \): Predicted probability for the \( i \)-th sample.
- \( y^{(i)} \): True label (0 or 1).
Intuition Behind Log Loss
- If the prediction is close to the true label (\( \sigma(z) \approx y \)), the loss is small.
- If the prediction is far from the true label (\( \sigma(z) \neq y \)), the loss is large.
Imagine it as the wizardly rule: “The more confident and correct your prediction, the less you pay in energy costs!”
7. Gradient Descent for Optimization
To minimize the cost function and find the optimal weights \( \mathbf{w} \) and bias \( b \), we use Gradient Descent. This is the magical broomstick that sweeps us to the valley of minimal error.
Steps in Gradient Descent:
- Initialize weights and bias randomly.
- Compute the gradient of the cost function with respect to weights and bias: \[ \frac{\partial J}{\partial w_j} = \frac{1}{m} \sum_{i=1}^m \left( \sigma(z^{(i)}) - y^{(i)} \right) x_j^{(i)} \]
- Update the weights and bias: \[ w_j = w_j - \alpha \cdot \frac{\partial J}{\partial w_j} \] \[ b = b - \alpha \cdot \frac{\partial J}{\partial b} \] Here, \( \alpha \) is the learning rate.
- Repeat until convergence.
Gradient Descent Code Implementation:
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# Gradient Descent for Logistic Regression
def gradient_descent(X, y, lr=0.01, epochs=1000):
m, n = X.shape
w = np.zeros(n)
b = 0
for epoch in range(epochs):
# Linear combination
z = np.dot(X, w) + b
predictions = sigmoid(z)
# Gradients
dw = (1 / m) * np.dot(X.T, (predictions - y))
db = (1 / m) * np.sum(predictions - y)
# Update weights
w -= lr * dw
b -= lr * db
# Optional: Print cost every 100 epochs
if epoch % 100 == 0:
cost = -np.mean(y * np.log(predictions) + (1 - y) * np.log(1 - predictions))
print(f"Epoch {epoch}, Cost: {cost}")
return w, b
8. Multiclass Classification Using Logistic Regression (One-vs-All Approach)
Now, let’s level up! So far, we’ve been dealing with binary classification—predicting between two classes like “Friendly” vs. “Hostile.” But what if the magical creatures belong to more than two categories, like Dragons, Unicorns, and Goblins? 🐉🦄👹
Logistic Regression can handle this too, but with a twist! It uses techniques like One-vs-All (OvA) or One-vs-Rest (OvR) to adapt to multiclass classification.
What is One-vs-All (OvA)?
The One-vs-All approach turns a multiclass problem into multiple binary classification problems. For each class, the model asks:
“Is this creature Class A, or is it NOT Class A?”
Here’s how it works for \( k \) classes:
- Train a separate Logistic Regression model for each class.
- For class \( C_i \), consider it as the “positive” class and all other classes as “negative.”
- At prediction time, calculate the probabilities for all \( k \) models and pick the class with the highest probability.
Example: Classifying Dragons, Unicorns, and Goblins
Suppose you have the following classes:
- Class 1: Dragons 🐉
- Class 2: Unicorns 🦄
- Class 3: Goblins 👹
For each class, Logistic Regression learns a model:
- Is it a Dragon or not?
- Is it a Unicorn or not?
- Is it a Goblin or not?
During prediction:
- The model calculates probabilities for all three classes.
- Assigns the class with the highest probability.
Mathematical Formulation
For \( k \) classes and \( m \) training samples, let:
- \( \mathbf{X} \): Feature matrix (\( m \times n \)).
- \( \mathbf{y} \): Labels (\( 1, 2, \dots, k \)).
The model trains \( k \) binary classifiers:
- For class \( i \), predict \( P(y = i | \mathbf{x}) = \sigma(\mathbf{w}_i^\top \mathbf{x} + b_i) \).
At prediction:
\[ \hat{y} = \text{argmax}_i \, P(y = i | \mathbf{x}) \]Implementation of One-vs-All
Here’s a Python implementation using NumPy:
import numpy as np
# Sigmoid Function
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# Train Logistic Regression for One-vs-All
def train_ova(X, y, num_classes, lr=0.01, epochs=1000):
m, n = X.shape
weights = np.zeros((num_classes, n))
biases = np.zeros(num_classes)
for i in range(num_classes):
# Create binary labels for class i
y_binary = (y == i).astype(int)
# Initialize weights and bias for this class
w = np.zeros(n)
b = 0
for epoch in range(epochs):
# Linear combination
z = np.dot(X, w) + b
predictions = sigmoid(z)
# Gradients
dw = (1 / m) * np.dot(X.T, (predictions - y_binary))
db = (1 / m) * np.sum(predictions - y_binary)
# Update weights
w -= lr * dw
b -= lr * db
# Store weights and bias for this class
weights[i, :] = w
biases[i] = b
return weights, biases
# Predict with One-vs-All
def predict_ova(X, weights, biases):
probabilities = sigmoid(np.dot(X, weights.T) + biases)
return np.argmax(probabilities, axis=1)
9. Challenges in Logistic Regression
Logistic Regression is simple and effective but not without its challenges. Let’s explore a few:
1. Linearity in Features
- Logistic Regression assumes a linear relationship between features and the log-odds.
- If your data isn’t linearly separable, the model struggles.
Solution: Use feature engineering or transform your features (e.g., Polynomial Features).
2. Overfitting with High-Dimensional Data
- If the number of features is very large compared to the number of samples, the model may overfit.
Solution: Apply Regularization (e.g., L1 or L2 penalties).
3. Imbalanced Data
- If one class dominates the dataset, the model may become biased toward that class.
Solution: Use class weights or oversample the minority class.
4. Multicollinearity
- Highly correlated features can destabilize the model.
Solution: Use Principal Component Analysis (PCA) or remove redundant features.
Visualizing One-vs-All (Mermaid Diagram)
Here’s a diagram to visualize the One-vs-All approach:
graph TD A["Training Data"] --> B["Train Model for Class 1 (vs All)"] A --> C["Train Model for Class 2 (vs All)"] A --> D["Train Model for Class 3 (vs All)"] B --> E["P(Class 1)"] C --> F["P(Class 2)"] D --> G["P(Class 3)"] E --> H["Choose Class with Highest Probability"] F --> H G --> H
10. Challenges in Implementation
Let’s tackle some practical hurdles:
-
Convergence Issues in Gradient Descent
- If the learning rate \( \alpha \) is too high, the cost function may not converge.
- Solution: Start with a small \( \alpha \), monitor the cost, and adjust as needed.
-
Feature Scaling
- Features with large ranges dominate those with smaller ranges.
- Solution: Normalize or standardize features to have mean 0 and variance 1.
-
Interpretability of Coefficients
- Logistic Regression coefficients are not directly interpretable as probabilities.
- Solution: Use \( e^{w_j} \) to interpret coefficients as odds ratios.
11. Step-by-Step Code Implementation of Logistic Regression
Let’s bring everything together and implement a full Logistic Regression model from scratch. We’ll include:
- Binary classification using Gradient Descent.
- Multi-class classification with One-vs-All.
1. Import Required Libraries
We’ll start by importing essential libraries:
import numpy as np
import matplotlib.pyplot as plt
2. Sigmoid Function
The heart of Logistic Regression is the Sigmoid Function:
def sigmoid(z):
return 1 / (1 + np.exp(-z))
3. Binary Logistic Regression
Here’s how to train a Logistic Regression model for binary classification:
def train_logistic_regression(X, y, lr=0.01, epochs=1000):
m, n = X.shape
w = np.zeros(n) # Initialize weights
b = 0 # Initialize bias
for epoch in range(epochs):
# Compute predictions
z = np.dot(X, w) + b
predictions = sigmoid(z)
# Compute gradients
dw = (1 / m) * np.dot(X.T, (predictions - y))
db = (1 / m) * np.sum(predictions - y)
# Update weights and bias
w -= lr * dw
b -= lr * db
# Optional: Print cost every 100 epochs
if epoch % 100 == 0:
cost = -np.mean(y * np.log(predictions) + (1 - y) * np.log(1 - predictions))
print(f"Epoch {epoch}, Cost: {cost}")
return w, b
4. Predict Function
After training, use the model to make predictions:
def predict_binary(X, w, b, threshold=0.5):
z = np.dot(X, w) + b
probabilities = sigmoid(z)
return (probabilities >= threshold).astype(int)
5. Multi-Class Logistic Regression (One-vs-All)
For multiclass classification, we extend the binary implementation:
def train_multiclass_ova(X, y, num_classes, lr=0.01, epochs=1000):
m, n = X.shape
weights = np.zeros((num_classes, n))
biases = np.zeros(num_classes)
for i in range(num_classes):
# Create binary labels for class i
y_binary = (y == i).astype(int)
# Train binary classifier for class i
w, b = train_logistic_regression(X, y_binary, lr, epochs)
# Store weights and bias for class i
weights[i, :] = w
biases[i] = b
return weights, biases
def predict_multiclass(X, weights, biases):
z = np.dot(X, weights.T) + biases
probabilities = sigmoid(z)
return np.argmax(probabilities, axis=1)
Example Dataset
Let’s test our implementation on a simple dataset.
# Example Dataset (Binary Classification)
X = np.array([[1, 2], [2, 3], [3, 4], [5, 6], [6, 7], [8, 9]])
y = np.array([0, 0, 0, 1, 1, 1])
# Train the Binary Logistic Regression Model
w, b = train_logistic_regression(X, y, lr=0.1, epochs=1000)
# Predict
predictions = predict_binary(X, w, b)
print("Predictions:", predictions)
For multiclass classification:
# Example Dataset (Multiclass Classification)
X = np.array([[1, 2], [2, 3], [3, 4], [5, 6], [6, 7], [8, 9]])
y = np.array([0, 1, 2, 0, 1, 2])
# Train the Multiclass Logistic Regression Model
weights, biases = train_multiclass_ova(X, y, num_classes=3, lr=0.1, epochs=1000)
# Predict
predictions = predict_multiclass(X, weights, biases)
print("Multiclass Predictions:", predictions)
12. Summary of Key Takeaways
- Logistic Regression predicts probabilities for binary or multiclass classification problems.
- The Sigmoid Function maps linear inputs to probabilities between 0 and 1.
- Logistic Regression optimizes the Log Loss using techniques like Gradient Descent.
- For multiclass classification, the One-vs-All approach trains separate binary classifiers for each class.
13. References for Further Reading
Final Joke 🧙♂️
Why did the Sigmoid Function break up with the Linear Function?
Because it couldn’t handle the drama of going to infinity and beyond! 🤣