Logistic Regression From Scratch in Python: Deriving Sigmoid and Log Loss
Logistic Regression models binary probabilities by passing a linear combination of features through the Sigmoid function, optimizing weights using Binary Cross-Entropy (Log Loss) gradient descent.
Logistic Regression is a foundational supervised classification algorithm that estimates the probability of a discrete outcome (such as binary or ) by passing a linear combination of input features through the non-linear Sigmoid (logistic) function.
Imagine an oncologist evaluating diagnostic scans. The doctor does not deliver a blunt yes/no verdict immediately; instead, they weigh multiple risk factors (age, blood biomarkers, family history) to establish a calibrated degree of suspicion between 0% and 100%. Only when confidence crosses a defined medical threshold do they flag the case for further biopsy. Logistic Regression mirrors this exact clinical process: computing continuous probabilities first, and applying classification cutoffs second.
1. Why Linear Regression Fails for Classification
Using standard Ordinary Least Squares linear regression for binary classification presents two fatal flaws:
- Unbounded Outputs: Linear regression produces predictions anywhere on (e.g. or ), which cannot be interpreted as valid probabilities.
- Non-Convex Loss Surface: Pairing linear regression's Mean Squared Error (MSE) with non-linear probabilities produces a bumpy, non-convex cost landscape filled with local minima where gradient descent gets trapped.
2. The Mathematical Derivation (From Odds to Sigmoid)
1. Modeling the Log-Odds (Logit)
The ratio of the probability of an event happening () to it not happening () is called the Odds:
Taking the natural logarithm yields the Log-Odds (Logit), which maps probabilities across all real numbers . We set this equal to our linear combination:
Exponentiating and solving for yields the Sigmoid Function :
2. The Calculus of the Sigmoid Derivative
The derivative of the sigmoid function possesses a remarkably clean closed-form structure:
3. Binary Cross-Entropy Loss (Log Loss)
To guarantee a convex optimization landscape with a single global minimum, logistic regression minimizes Binary Cross-Entropy (derived from Maximum Likelihood Estimation):
- When True Class : Cost . If , cost . If (confidently wrong), cost .
- When True Class : Cost . Punishes confident false alarms exponentially.
4. Gradient Descent Update Rules
Applying the chain rule through Cross-Entropy and the Sigmoid derivative produces the exact gradient form as linear regression:
4. Worked Numerical Example (By Hand)
Let's perform 1 update step for 2 samples with initial parameters and learning rate :
- Sample 1: . Error: .
- Sample 2: . Error: .
- Weight Gradient: .
- Bias Gradient: .
- Updated Parameters: .
5. Decision Threshold Strategy Matrix
| Threshold | False Positive Rate | False Negative Rate | Ideal Real-World Use Case |
|---|---|---|---|
| (Balanced) | Balanced | Balanced | Standard benchmark binary classification problems. |
| (High Recall) | Higher (more false alarms) | Extremely Low (catches nearly all positives) | Cancer screening, credit card fraud detection, critical safety alerts. |
| (High Precision) | Extremely Low (rare false alarms) | Higher (misses borderline cases) | Automatic spam email deletion, high-budget targeted marketing campaigns. |
6. Code: Logistic Regression in Python
1. NumPy Implementation From Scratch
import numpy as np
X = np.array([1, 2, 3, 4, 5, 6], dtype=float)
y = np.array([0, 0, 0, 1, 1, 1], dtype=float)
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-np.clip(z, -250, 250)))
def train_logistic_regression(X, y, alpha=0.1, epochs=3000):
m = len(X)
w, b = 0.0, 0.0
for _ in range(epochs):
z = w * X + b
y_pred = sigmoid(z)
error = y_pred - y
# Gradients
dw = (1 / m) * np.sum(error * X)
db = (1 / m) * np.sum(error)
w -= alpha * dw
b -= alpha * db
return w, b
w_fit, b_fit = train_logistic_regression(X, y)
print(f"Learned Weight: {w_fit:.4f}, Learned Bias: {b_fit:.4f}")
# Prediction helper with customizable decision threshold
def predict(x_val, w, b, threshold=0.5):
prob = sigmoid(w * x_val + b)
return int(prob >= threshold), prob
label, prob = predict(3.5, w_fit, b_fit, threshold=0.5)
print(f"Prediction for x=3.5 -> Probability: {prob:.3f}, Assigned Class: {label}")2. Production Scikit-Learn
from sklearn.linear_model import LogisticRegression
X_2d = X.reshape(-1, 1)
model = LogisticRegression()
model.fit(X_2d, y)
# Evaluating probabilities vs hard predictions
prob_sample = model.predict_proba([[3.5]])[0]
print(f"Class 0 Probability: {prob_sample[0]:.3f}, Class 1 Probability: {prob_sample[1]:.3f}")
print("Predicted Class:", model.predict([[3.5]])[0])Summary
- Logistic regression models binary probabilities by mapping linear inputs through the Sigmoid function .
- Binary Cross-Entropy (Log Loss) guarantees a convex optimization surface, penalizing confident wrong predictions exponentially.
- The gradient mirrors linear regression, enabling efficient gradient descent parameter updates.
- The classification decision threshold can be tuned away from to optimize precision or recall based on domain requirements.
Common questions
Why is Logistic Regression called 'regression' if it is used for classification?
It is named logistic regression because it computes a continuous weighted sum of features (linear regression) before passing the log-odds through the sigmoid function to output calibrated probabilities.
Why is Binary Cross-Entropy used instead of Mean Squared Error (MSE)?
Combining MSE with the non-linear Sigmoid creates a non-convex loss function with multiple local minima. Binary Cross-Entropy produces a perfectly convex surface, guaranteeing that gradient descent finds the global minimum.
What is the derivative of the Sigmoid function?
The derivative of the sigmoid function sigma(z) is sigma'(z) = sigma(z) * (1 - sigma(z)), which simplifies the gradient of binary cross-entropy into the clean form (y_pred - y) * x.
How do you choose the decision threshold in Logistic Regression?
While 0.5 is the default threshold, you lower the threshold (0.2-0.3) in high-risk settings like medical diagnosis to maximize recall, or raise it (0.7-0.8) in spam detection to maximize precision.