Logistic Regression
Objectives
Section titled “Objectives”After this lesson, you will be able to:
- Understand why binary classification requires a model predicting probabilities, and why thresholding linear regression outputs fails.
- Interpret logistic regression from three viewpoints: probability via the sigmoid function, linear log-odds, and the decision boundary .
- Derive the log loss function from maximum likelihood and calculate its gradient .
- Implement logistic regression using gradient descent and scikit-learn, configure the
Cparameter, and interpret coefficients via odds ratios.
Prerequisites
Section titled “Prerequisites”- Linear Regression, in Machine Learning (Learned)
- Gradient Descent, in Machine Learning (Learned)
- Probability and Statistics, in Math for AI(Not translated yet) (Learned)
Intuition
Section titled “Intuition”A class advisor recorded the weekly study hours of 18 students before an exam, along with whether each passed or failed. The numbers below represent synthetic data generated for this lesson.
| Result | Weekly Study Hours |
|---|---|
| Fail (9 students) | 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4.5, 6 |
| Pass (9 students) | 4, 5, 5.5, 6.5, 7, 7.5, 8, 9, 10 |
At a glance: studying less often leads to failing, studying more often leads to passing, with an overlapping region in between (a student with 4 hours passed, while students with 4.5 and 6 hours failed). The teacher wants to answer: if a student studies for 5 hours, what is their probability of passing?
This is a classification problem: the label takes only two values, 1 (pass) or 0 (fail). What the teacher needs is not just a discrete “pass or fail”, but a probability: 90% and 51% both predict “pass”, but carry vastly different confidence levels.
Trying Linear Regression with a Threshold
Section titled “Trying Linear Regression with a Threshold”The first intuitive attempt is to treat 0 and 1 as numerical targets, fit an ordinary least squares line as in Linear Regression, and predict “pass” whenever the predicted value is at least 0.5. For our 18 students, the fitted line is . The 0.5 threshold falls at hours, correctly classifying 16 out of 18 students. While seemingly reasonable, this approach suffers from two fundamental flaws.
First, predictions are not valid probabilities. A student studying 0 hours receives , while 10 hours yields . Probabilities below 0 or above 1 are mathematically meaningless.
Second, the line is distorted by “overly correct” points. Suppose a student studies for 30 hours and naturally passes. This extreme point makes the classification problem no harder. Yet the regression line must stretch to reach , tilting the line downward: . The 0.5 threshold shifts from 4.83 to 5.53 hours. Consequently, students studying 5 and 5.5 hours—who actually passed—are now misclassified as failures: accuracy on the original 18 students drops to 14/18. Mean squared error penalizes the 30-hour student simply because exceeds 1, even though in classification, being “confidently correct” should never be penalized.
Squashing Outputs into (0, 1)
Section titled “Squashing Outputs into (0, 1)”What we need is an S-shaped curve: hovering near 0 for very low study hours, approaching 1 for high study hours, and transitioning smoothly from 0 to 1 through the overlapping zone. At both extremes, the curve must flatten horizontally, so an extreme point at 30 hours that is already near 1 exerts almost no pull on the curve. This is the sigmoid curve, and logistic regression is the method for fitting it.
The objective is: given feature values of a sample, estimate the probability that it belongs to class 1 using an S-shaped curve, choosing parameters that maximize the likelihood of observing the training data.
Theory
Section titled “Theory”Problem Formulation
Section titled “Problem Formulation”We have training samples. The -th sample has a feature vector with dimensions and a label . Class 1 is called the positive class, and class 0 is the negative class. For our 18 students, , , and is study hours.
Logistic regression proceeds in two stages. The first stage mirrors linear regression by computing a linear score:
where represents weights and is the intercept (bias). Here can take any real value and is called the logit. The second stage passes through the sigmoid function to produce a valid probability:
Unlike linear regression where is a continuous target prediction, here represents the model’s estimated probability for class 1. The probability of class 0 is simply .
The Sigmoid Function
Section titled “The Sigmoid Function”Inspecting the formula reveals why it produces an S-shaped curve:
- When : , so .
- When : , so .
- When : , so .
Because for all real , the denominator strictly exceeds 1, guaranteeing that lies strictly inside the open interval , never reaching exactly 0 or 1. The function is symmetric around : . Representative values include:
| 0.0025 | 0.018 | 0.1192 | 0.5 | 0.8808 | 0.982 | 0.9975 |
The curve is steepest around , and flattens out almost entirely once : exactly the behavior desired in the Intuition section. The diagram in the Visualization section illustrates this curve.
Odds and Log-Odds: Interpreting the Model Linearly
Section titled “Odds and Log-Odds: Interpreting the Model Linearly”The odds of a probability is the ratio : the probability of occurrence divided by non-occurrence. A probability of 0.8 corresponds to odds of (“4 to 1”). A probability of 0.5 has odds of 1. Odds range from 0 to .
Inverting allows us to express in terms of . Invert both sides, subtract 1, and take the natural logarithm:
The second step subtracts 1 from both sides and takes a common denominator: . The third step flips the fraction (turning into ) and takes . The left-hand side is the log-odds, also called the logit of . Hence:
This formulation provides a clear interpretation for weights. Increasing by 1 unit while holding other features constant increases log-odds by exactly , meaning odds are multiplied by :
In our first code cell, fitting the 18 students gives and . Each additional hour of study multiplies the odds of passing by . A student studying 4 hours has with odds 0.386; studying 5 hours gives with odds 1.365, exactly greater. Notice that probability does not increase linearly: moving from 4 to 5 hours boosts probability by 0.3, while moving from 9 to 10 hours changes it minimally as it approaches saturation near 1. If is negative, , reducing the odds.
Decision Boundary
Section titled “Decision Boundary”To assign a discrete predicted class, we compare against a threshold, typically 0.5 by default: predicting class 1 when . Because is strictly monotonic and :
The set of points where defines the decision boundary. For one feature, it is a single point: hours. For two features, it is a line ; for features, it is a hyperplane. The non-linear S-curve only serves to map real numbers to probabilities; the decision boundary itself is always flat (linear). Thus, logistic regression is fundamentally a linear classifier.
The 0.5 threshold is merely a default. When missing a positive case carries greater penalty than a false alarm (e.g., medical diagnosis), one lowers the threshold. Choosing thresholds and evaluating classifiers beyond simple accuracy is covered in Model Evaluation: Accuracy, Precision, Recall, F1.
Loss Function: From Maximum Likelihood to Log Loss
Section titled “Loss Function: From Maximum Likelihood to Log Loss”We need an objective function measuring model performance. The principle of maximum likelihood states: choose parameters that make the observed data most probable under the model.
Step 1: Probability of an individual sample. The model specifies and . We unify both cases into a single concise expression:
Verification: when , the second factor becomes , leaving . When , the first factor becomes , leaving . This is the Bernoulli distribution.
Step 2: Probability of the entire dataset. Assuming training samples are independent and identically distributed, the joint probability of observing all labels is the product:
This quantity, viewed as a function of the parameters, is the likelihood. We seek to maximize it.
Step 3: Taking the logarithm. The product of many numbers strictly less than 1 rapidly underflows to 0. For the optimal model on our 18 students, likelihood is merely 0.0062; with thousands of samples, floating-point arithmetic rounds it to zero. Because is strictly increasing, maximizing is equivalent to maximizing . Applying and :
Step 4: Converting to a minimization problem. In machine learning, loss functions are conventionally minimized. Negating the log-likelihood and averaging over samples yields log loss, also known as binary cross-entropy:
Minimizing directly maximizes likelihood since .
Examining each sample: every point contributes of the probability assigned to its true label. Assigning 0.9 to the true label incurs a loss of 0.105; 0.5 incurs 0.693; 0.1 incurs 2.303; approaching 0 incurs nearly infinite penalty. Log loss heavily punishes confident incorrect predictions. Among our 18 students, the two most heavily penalized samples are the two misclassified students: the one studying 4 hours who passed (, loss ) and the one studying 6 hours who failed (, loss ). Averaged over all 18 students, . An uninformative model with giving every student 0.5 yields .
Why Not Mean Squared Error
Section titled “Why Not Mean Squared Error”Why not retain MSE with ? Consider a sample with and examine its loss as a function of :
- MSE: . As , it plateaus near 1; as , it plateaus near 0. A function that transitions from flat, down a slope, and back to flat is S-shaped, not a convex bowl. When summing over multiple samples, the loss surface contains extensive plateaus where gradient descent crawls extremely slowly.
- Log loss: . As , it increases asymptotically along the straight line , without plateauing. The worse the error, the steeper the slope.
Numerically: at (, a confident error), the derivative with respect to under MSE is merely , whereas under log loss it is —roughly 200 times stronger. The following derivation shows why.
Gradient of Log Loss
Section titled “Gradient of Log Loss”Gradient descent requires the partial derivatives of with respect to each and . Consider a single sample, omitting the superscript: , with and . By the chain rule:
Step 1: Derivative of sigmoid. Writing , the derivative of is , multiplied by the derivative of , which is :
The second step factors the fraction. The third step uses . Thus . This derivative peaks at 0.25 at and vanishes at both extremes: this explains why MSE paired with sigmoid saturates and stalls.
Step 2: Derivative of with respect to . Differentiating gives ; differentiating with respect to gives :
The second step finds a common denominator. The third expands the numerator: .
Step 3: Putting it together. Since , we have and . Multiplying all three terms:
The factor in the numerator and denominator cancels out completely. This is why log loss does not saturate when making severe mistakes: the derivative with respect to simplifies cleanly to .
Step 4: Averaging over samples. Because is the mean of individual sample losses , its derivative is the mean of individual gradients:
In compact matrix notation with feature matrix () and vector of predicted probabilities: .
Training with Gradient Descent
Section titled “Training with Gradient Descent”Setting the gradient to zero yields transcendental equations involving , which lack closed-form analytical solutions like the normal equation. We turn to gradient descent:
The reassuring fact: log loss is convex in . As a function of , a sample loss has derivative and second derivative , curving upward like a bowl. Since is linear in the parameters, the sum across all samples remains strictly convex. There are no local minima to get trapped in. By default, scikit-learn uses L-BFGS rather than standard gradient descent, leveraging curvature approximations to converge in fewer iterations to the same global optimum.
Regularization in scikit-learn
Section titled “Regularization in scikit-learn”Scikit-learn’s LogisticRegression includes L2 regularization by default, similar to Ridge regression in Ridge, Lasso, and Elastic Net. In version 1.8, the objective function (for uniformly weighted samples) is formulated as:
Cis the inverse of regularization strength. Comparing with in the Regularization lesson, . SmallerCimposes stronger regularization, whereas largerCweakens it. The default isC=1.- is not regularized (with the default
lbfgssolver). - To disable regularization, scikit-learn 1.8 recommends
C=np.inf(thepenaltyparameter is deprecated). Because version 1.8.0 emits an extraneous warning withC=np.inf, our code examples useC=1e10: penalty is negligible and does not alter results. - Features should be standardized, for the same reason as Ridge: regularization penalizes all equally.
Regularization also prevents pathology in a critical scenario: when classes are linearly separable. In that case, doubling and leaves the boundary unchanged while pushing every closer to 0 or 1, further driving log loss down. There is no finite minimum: weights inflate indefinitely. The third code example demonstrates this phenomenon.
Limitations
Section titled “Limitations”- Decision boundaries are always linear. If two classes form concentric circles, no straight boundary can separate them. One must engineer features like (akin to polynomial regression), or switch to non-linear algorithms as in K-Nearest Neighbors and Decision Trees and Random Forests.
- Limited to binary classification natively. For multi-class classification, softmax replaces sigmoid, as covered in Softmax Regression.
- Probabilities are trustworthy only if the model is well-specified. If true log-odds are non-linear with respect to features, estimated probabilities may be miscalibrated even when classification accuracy remains high.
- Weights are hard to interpret under multicollinearity (see the third pitfall below).
Visualization
Section titled “Visualization”The figure below plots the sigmoid function against . Observe the two ends of the curve flattening horizontally toward horizontal asymptotes at 0 and 1, and the inflection point at . Dots represent the 18 students positioned at from the first code cell, at height 1 if passing and 0 if failing. The vertical line corresponds to the decision boundary at 4.75 study hours.
Code Examples
Section titled “Code Examples”The three cells below run independently, each generating its own dataset. The first execution takes slightly longer while the browser loads scikit-learn; the second cell also loads Matplotlib.
Gradient Descent with NumPy
Section titled “Gradient Descent with NumPy”The first cell implements the theory from scratch: sigmoid, log loss, gradient , starting from with , stopping when gradient magnitude drops below . Finally, scikit-learn’s LogisticRegression is run side-by-side for comparison.
Logistic regression via gradient descent
import numpy as npfrom sklearn.linear_model import LogisticRegression
# 18 students (made-up data): hours of study and result (1 = passed).hours = np.array([0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 9, 10])passed = np.array([0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1])
def sigmoid(z): return 1 / (1 + np.exp(-z))
def log_loss(p, y): # binary cross-entropy, averaged over m samples return -np.mean(y * np.log(p) + (1 - y) * np.log(1 - p))
w, b, eta = 0.0, 0.0, 0.2 # start at 0, learning rate 0.2for step in range(100_000): p = sigmoid(w * hours + b) # y^ = P(pass | hours) grad_w = np.mean((p - passed) * hours) # dL/dw grad_b = np.mean(p - passed) # dL/db if np.hypot(grad_w, grad_b) < 1e-6: # stop when the gradient is small break w, b = w - eta * grad_w, b - eta * grad_bprint(f"GD stopped after {step} steps")print(f"GD: w = {w:.3f}, b = {b:.3f}, log loss = {log_loss(p, passed):.4f}")print(f"threshold -b/w = {-b / w:.3f} h, odds ratio e^w = {np.exp(w):.2f}")for h in [2, 4, 6, 8]: print(f"P(pass | {h} h) = {sigmoid(w * h + b):.3f}")
# scikit-learn: a huge C means almost no penalty (see the lesson).model = LogisticRegression(C=1e10).fit(hours.reshape(-1, 1), passed)w_sk, b_sk = model.coef_[0, 0], model.intercept_[0]print(f"sklearn: w = {w_sk:.3f}, b = {b_sk:.3f}")print(f"sklearn P(pass | 4 h) = {model.predict_proba([[4]])[0, 1]:.3f}")The first run downloads Python, about 10 MB.
GD stops after 7540 steps with , , and log loss 0.2826, matching scikit-learn to three decimal places. The decision boundary is hours, with each extra hour of study multiplying pass odds by 3.54. Pass probabilities evaluate to 0.030, 0.278, 0.828, and 0.984 for 2, 4, 6, and 8 hours: increasing fastest near the boundary and saturating at both ends, tracing an S-curve.
Over 7500 steps is relatively high because hours were unstandardized: as shown in Gradient Descent, features far from zero create elongated loss valleys. Changing eta to 0.05 converges to the exact same values, but requires roughly 30,000 steps.
Straight Line vs. S-Curve
Section titled “Straight Line vs. S-Curve”The second cell verifies the numerical observations from the Intuition section: fitting linear regression and logistic regression on 18 students, then repeating on 19 students (including an outlier studying 30 hours), printing thresholds and accuracy on the original 18, and plotting all four models.
Linear regression vs. logistic regression
import numpy as npimport matplotlib.pyplot as pltfrom sklearn.linear_model import LinearRegression, LogisticRegression
# The 18 students from the first cell, plus one who studied 30 h and passed.hours = np.array([0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 9, 10])passed = np.array([0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1])X, X30 = hours.reshape(-1, 1), np.append(hours, 30).reshape(-1, 1)y30 = np.append(passed, 1)
grid = np.linspace(0, 32, 300).reshape(-1, 1)fig, ax = plt.subplots(figsize=(7, 4))ax.scatter(X30[:, 0], y30, color="black", zorder=3, label="students")for name, Xt, yt, style in [("18", X, passed, "-"), ("19", X30, y30, "--")]: lin = LinearRegression().fit(Xt, yt) log = LogisticRegression(C=1e10).fit(Xt, yt) # almost no penalty a, c = lin.coef_[0], lin.intercept_ w, b = log.coef_[0, 0], log.intercept_[0] acc = np.mean((lin.predict(X) >= 0.5) == passed) # first 18 only print(f"{name} students: line y = {a:.4f} x + {c:.4f}") print(f" line: y^(0) = {c:.3f}, y^(10) = {a * 10 + c:.3f}, " f"y^(30) = {a * 30 + c:.3f}") print(f" line threshold = {(0.5 - c) / a:.2f} h, accuracy = {acc:.3f}") print(f" logistic threshold = {-b / w:.2f} h, " f"accuracy = {log.score(X, passed):.3f}") ax.plot(grid, lin.predict(grid), "tab:orange", ls=style, label=f"linear, {name} students") ax.plot(grid, log.predict_proba(grid)[:, 1], "tab:blue", ls=style, label=f"logistic, {name} students")ax.axhline(0.5, color="gray", lw=0.8)ax.set_xlabel("hours of study")ax.set_ylabel("prediction")ax.set_ylim(-0.3, 1.5)ax.legend(fontsize=8)plt.show()The first run downloads Python, about 10 MB.
On 18 students, both approaches yield close thresholds (4.83 vs. 4.75 hours) and identical accuracy of 0.889 (16/18). However, linear regression predicts at 0 hours, at 10 hours, and at 30 hours: non-interpretable as probabilities. Adding the 30-hour student tilts the line down, shifting the threshold to 5.53 hours and reducing accuracy on the original 18 students to 0.778. Meanwhile, the sigmoid curve barely moves: its threshold remains 4.75 hours. The explanation lies in the gradient: the 30-hour student has close to 1, making , exerting virtually zero gradient push.
Real-World Data: Breast Cancer Diagnosis
Section titled “Real-World Data: Breast Cancer Diagnosis”The third cell uses the built-in load_breast_cancer dataset: 569 tumor samples with 30 features computed from cell nuclei images (radius, texture, concavity…). Label 1 indicates benign, and label 0 indicates malignant. The code splits into 75% train and 25% test stratified by class (stratify=y), places StandardScaler in a pipeline, fits with C=1 (default) and C=1e10 (essentially unregularized), and inspects the three largest coefficients of the default model.
Logistic regression on breast cancer dataset
import numpy as npfrom sklearn.datasets import load_breast_cancerfrom sklearn.linear_model import LogisticRegressionfrom sklearn.model_selection import train_test_splitfrom sklearn.pipeline import make_pipelinefrom sklearn.preprocessing import StandardScaler
# 569 tumors, 30 features. Labels: 0 = malignant, 1 = benign.data = load_breast_cancer()X, y = data.data, data.targetprint(f"m = {X.shape[0]}, n = {X.shape[1]}, benign = {y.mean():.3f}")X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, stratify=y, random_state=0)
for C in [1.0, 1e10]: # default (L2 penalty, C = 1) and almost no penalty model = make_pipeline(StandardScaler(), LogisticRegression(C=C, max_iter=10_000)) model.fit(X_train, y_train) w = model[-1].coef_[0] print(f"C = {C:g}: train acc = {model.score(X_train, y_train):.3f}, " f"test acc = {model.score(X_test, y_test):.3f}, " f"max |w| = {np.abs(w).max():.1f}")
# Default model: the 3 largest coefficients in absolute value.model = make_pipeline(StandardScaler(), LogisticRegression())model.fit(X_train, y_train)w = model[-1].coef_[0]for j in np.argsort(-np.abs(w))[:3]: print(f"{data.feature_names[j]:>19}: w = {w[j]:+.3f}, " f"e^w = {np.exp(w[j]):.3f}")
# Probability of benign for the first 5 tumors of the test set.proba = model.predict_proba(X_test[:5])[:, 1]print("P(benign):", " ".join(f"{p:.3f}" for p in proba))print("predicted:", model.predict(X_test[:5]), " true:", y_test[:5])The first run downloads Python, about 10 MB.
- Default
C=1: test accuracy 0.958 (137/143 tumors), with the largest coefficient magnitude at only 1.2. C=1e10: training accuracy reaches 1.000, revealing a linearly separating hyperplane on training data, causing the largest coefficient to balloon to 205.9, as predicted in the Regularization section. Test accuracy drops to 0.944 due to overfitting.- The top three coefficients are all negative: radius error (fluctuation of nucleus radius, ), worst texture (roughness in the worst region, ), mean concave points (average number of concave indentations, ). Because features are standardized: increasing radius error by one standard deviation (holding other features constant) multiplies the odds of being benign by —less than a third. Negative weights here indicate malignant risk, since class 1 denotes benign.
- The first five test tumors have benign probabilities of 0.996, 0.000, 0.000, 1.000, and 0.003, all correctly predicted. The “0.000” and “1.000” are artifacts of rounding; sigmoid never evaluates to absolute 0 or 1.
Common Pitfalls
Section titled “Common Pitfalls”Quick Check
Section titled “Quick Check”Summary and Further Reading
Section titled “Summary and Further Reading”Key Takeaways:
- Logistic regression computes and applies sigmoid: . Unlike straight lines, predictions stay strictly within and remain robust to extreme, confidently correct points.
- Log-odds are linear: . Increasing by one unit scales odds by . The decision boundary at threshold 0.5 is the linear hyperplane .
- Log loss is times the log-likelihood: This loss function is convex, heavily penalizes confident errors, and has gradient , mirroring linear regression. Since no closed-form solution exists, training relies on gradient descent or L-BFGS.
LogisticRegressionapplies L2 regularization by default withC=1, : smallerCmeans stronger regularization. Regularization keeps coefficients bounded when data is linearly separable.
Further Reading:
- Vũ Hữu Tiệp (2017). Machine Learning cơ bản, Bài 10: Logistic Regression. Clear derivation of loss from likelihood and Python examples with study hours. Uses slightly different notation: for sample count, for sigmoid output, and unscaled sum for loss .
- scikit-learn developers (2007–2026). scikit-learn 1.8 User Guide: Logistic regression, Section 1.1.11. Exact loss formulation including
C, solver comparisons, and default regularization rationales. - Jurafsky, D., Martin, J. H. (2026). Speech and Language Processing, 3rd ed. draft, Chapter 4: Logistic Regression and Text Classification (PDF). Rigorous derivation of cross-entropy and gradients with text classification examples.
- Ma, T., Ng, A. (2026). CS229 Lecture Notes (PDF), Section 2.1: Logistic regression. Gradient derivation from log-likelihood, using for and maximizing log-likelihood with gradient ascent.
- Cox, D. R. (1958). The Regression Analysis of Binary Sequences. Journal of the Royal Statistical Society: Series B 20(2), 215–232. Landmark paper introducing logistic regression to modern statistical methodology.
Your browser is blocking storage. Your progress will be lost when you reload the page.
Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About