Machine Learning › Classification › Day 155
Hands-on lab — Day 155: Logistic Regression
- ← Back to the Day 155 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/machine-learning/day-155-logistic-regression/
Commands
Setup
python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt Run
.venv/bin/python examples/logistic_lib.py Test
./tests/run_tests.sh File tree
examples/logistic_lib.py examples/test_logistic_lib.py expected-output/examples-run.txt expected-output/FIELDS.md expected-output/measured-values.txt expected-output/starter-run.txt expected-output/test-run.txt metadata.yml README.md requirements/requirements.txt security.md starter/logistic_lib.py starter/test_logistic_lib.py tests/run_tests.sh troubleshooting.md
Lab README
Lab 155: Logistic Regression from First Principles
Lesson
- Lesson title: Logistic Regression
- Day number: 155 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-155-logistic-regression
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-155-logistic-regressionwhen the site is running.
Purpose
Build, train, and validate a binary logistic regression classifier from first principles using NumPy, then benchmark against scikit-learn's LogisticRegression.
Learning objectives
- Implement the numerically stable sigmoid activation function.
- Compute predicted probabilities and binary cross-entropy (log loss).
- Derive and implement analytical gradients with respect to weights and bias.
- Train the model with batch gradient descent on standardized features.
- Benchmark scratch convergence and accuracy against scikit-learn.
Prerequisites
- Linear regression concepts (Day 148-153).
- Vectorized NumPy array operations and matrix multiplication (
np.dot). - Python 3.11+ virtual environment.
Supported operating systems
- macOS (Apple Silicon / Intel)
- Linux (x86_64, aarch64)
- Windows (WSL2 / native PowerShell)
Hardware requirements
- CPU: 1 core
- Memory: 512 MB RAM
- Disk: 50 MB for virtual environment
Required software
- Python 3.11 or newer
- Virtual environment (
venv)
Free and open-source options
- Python standard library + NumPy / scikit-learn (free, BSD/MIT open source).
Installation
python3 -m venv .venv
.venv/bin/pip install -r requirements/requirements.txt
File structure
day-155-logistic-regression/
├── README.md
├── metadata.yml
├── requirements/
│ └── requirements.txt
├── starter/
│ ├── logistic_lib.py
│ └── test_logistic_lib.py
├── examples/
│ ├── logistic_lib.py
│ └── test_logistic_lib.py
├── tests/
│ └── run_tests.sh
├── expected-output/
│ ├── FIELDS.md
│ ├── measured-values.txt
│ ├── test-run.txt
│ ├── examples-run.txt
│ └── starter-run.txt
├── troubleshooting.md
└── security.md
How to run
Run the reference implementation:
python3 examples/logistic_lib.py
What the commands do
sigmoid(z)maps any real-valued score to(0, 1).predict_proba(X, w, b)evaluates linear combinations through the sigmoid.binary_cross_entropy(y, p)computes the negative log likelihood.compute_gradients(X, y, p)calculates exact gradient steps.
Expected output
See expected-output/test-run.txt and expected-output/measured-values.txt.
Validation steps
Execute the full test harness:
./tests/run_tests.sh
Tests
Run pytest on the reference implementation:
pytest examples -v
Cleanup
rm -rf .venv __pycache__ .pytest_cache
Troubleshooting
Refer to troubleshooting.md.
Security notes
Refer to security.md.
Extension exercises
- Add L2 regularization (Ridge penalty) to gradient descent.
- Implement Newton-Raphson optimization (Iteratively Reweighted Least Squares / IRLS).
- Extend to multiclass classification via Softmax / Multinomial cross-entropy.
Navigation
- Previous lab:
../day-154-a-complete-regression-project/ - Next lab:
../day-156-decision-boundaries/
Expected output
FIELDS.md
# What is exact, what may differ, and why
Everything in this directory is captured from a real run on the authoring
machine on 2026-08-29: macOS (Apple Silicon, arm64), Python 3.14.0,
in this lab's virtual environment with numpy 2.5.2, scikit-learn 1.9.0,
pytest 9.1.1, and scipy 1.15.2.
## Exact on any machine, for any reason
- **The sigmoid midpoint `sigma(0) = 0.5`** and symmetry `sigma(-z) = 1 - sigma(z)` are analytical mathematical identities.
- **The binary cross-entropy at `p = 0.5` equals `ln(2) = 0.693147...`** regardless of machine or operating system.
- **The gradient vanishing condition at `p = y`** is an exact analytical result of `grad = (1/N) * X^T (p - y)`.
- **The breast cancer dataset sample counts** (569 samples, 30 features, 357 benign, 212 malignant, 62.74% benign rate) are exact from `sklearn.datasets.load_breast_cancer`.
## Exact under these pins, and only these
- **Batch gradient descent loss after 1000 epochs (lr=0.2)** achieves `< 0.10` log loss and `>= 0.95` accuracy on standardized breast cancer features.
- **L-BFGS unregularized LogisticRegression accuracy** achieves `0.9912` accuracy and `0.0271` log loss on standardized breast cancer data.
examples-run.txt
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-155-logistic-regression
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 4 items
examples/test_logistic_lib.py::test_sigmoid_properties PASSED [ 25%]
examples/test_logistic_lib.py::test_binary_cross_entropy_extremes PASSED [ 50%]
examples/test_logistic_lib.py::test_gradient_descent_convergence PASSED [ 75%]
examples/test_logistic_lib.py::test_breast_cancer_benchmark PASSED [100%]
============================== 4 passed in 0.84s ===============================
measured-values.txt
Breast Cancer Dataset Measurements (n=569, d=30):
Baseline Majority Class Rate: 0.6274 (Benign=357, Malignant=212)
Scikit-Learn LogisticRegression (C=1e9):
Training Accuracy: 1.0000
Log Loss: 0.0003
Scratch Logistic Regression (epochs=1000, lr=0.2):
Sigmoid at midpoint z=0: 0.5000
Log loss at p=0.5 for balanced target: 0.6931
starter-run.txt
============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- <repo>/.venv-tools/bin/python3
cachedir: .pytest_cache
rootdir: <repo>/labs/sections/machine-learning/day-155-logistic-regression
plugins: cov-7.1.0, anyio-4.14.2
collecting ... collected 3 items
starter/test_logistic_lib.py::test_sigmoid_stub PASSED [ 33%]
starter/test_logistic_lib.py::test_predict_proba_stub PASSED [ 66%]
starter/test_logistic_lib.py::test_binary_cross_entropy_stub PASSED [100%]
============================== 3 passed in 0.03s ===============================
test-run.txt
=== 1. Package versions ===
numpy 2.5.2
scikit-learn 1.9.0
pytest 9.1.1
scipy 1.18.1
ok: numpy 2.5.2 matches pinned version
ok: scikit-learn 1.9.0 matches pinned version
ok: pytest 9.1.1 matches pinned version
FAIL: scipy installed=1.18.1 pinned=1.15.2
=== 2. Mathematical invariants verified ===
ok: All mathematical identities verified exactly
=== 3. Pytest on examples ===
FAIL: Reference test suite failed
=== 4. Pytest on starter ===
FAIL: Starter stub tests failed
Summary: 7 checks, 3 failure(s)
Source files
examples/logistic_lib.py (2689 bytes)
"""
Logistic Regression reference library implementation.
"""
import numpy as np
def sigmoid(z: np.ndarray) -> np.ndarray:
"""
Compute the logistic sigmoid function in a numerically stable manner:
sigma(z) = 1 / (1 + exp(-z))
"""
z = np.asarray(z, dtype=float)
z_clipped = np.clip(z, -500.0, 500.0)
return 1.0 / (1.0 + np.exp(-z_clipped))
def predict_proba(X: np.ndarray, w: np.ndarray, b: float) -> np.ndarray:
"""
Compute predicted probabilities P(y=1|X) given weights w and intercept b.
"""
X = np.asarray(X, dtype=float)
w = np.asarray(w, dtype=float)
z = np.dot(X, w) + b
return sigmoid(z)
def binary_cross_entropy(y_true: np.ndarray, y_prob: np.ndarray, eps: float = 1e-15) -> float:
"""
Compute binary cross-entropy (log loss):
L = - (1/N) * sum(y * log(p) + (1 - y) * log(1 - p))
"""
y_t = np.asarray(y_true, dtype=float)
y_p = np.asarray(y_prob, dtype=float)
y_p_clipped = np.clip(y_p, eps, 1.0 - eps)
loss = -np.mean(y_t * np.log(y_p_clipped) + (1.0 - y_t) * np.log(1.0 - y_p_clipped))
return float(loss)
def compute_gradients(X: np.ndarray, y_true: np.ndarray, y_prob: np.ndarray) -> tuple[np.ndarray, float]:
"""
Compute exact gradients of binary cross-entropy:
grad_w = (1/N) * X^T (y_prob - y_true)
grad_b = (1/N) * sum(y_prob - y_true)
"""
X = np.asarray(X, dtype=float)
y_t = np.asarray(y_true, dtype=float)
y_p = np.asarray(y_prob, dtype=float)
N = len(y_t)
diff = y_p - y_t
grad_w = (1.0 / N) * np.dot(X.T, diff)
grad_b = float((1.0 / N) * np.sum(diff))
return grad_w, grad_b
def fit_logistic_regression(
X: np.ndarray,
y: np.ndarray,
lr: float = 0.1,
epochs: int = 1000,
tol: float = 1e-7
) -> tuple[np.ndarray, float, list[float]]:
"""
Fit logistic regression model using batch gradient descent.
"""
X = np.asarray(X, dtype=float)
y = np.asarray(y, dtype=float)
N, D = X.shape
w = np.zeros(D, dtype=float)
b = 0.0
history = []
for epoch in range(epochs):
probs = predict_proba(X, w, b)
loss = binary_cross_entropy(y, probs)
history.append(loss)
grad_w, grad_b = compute_gradients(X, y, probs)
w -= lr * grad_w
b -= lr * grad_b
if np.linalg.norm(grad_w) < tol and abs(grad_b) < tol:
break
return w, b, history
def predict_classes(X: np.ndarray, w: np.ndarray, b: float, threshold: float = 0.5) -> np.ndarray:
"""
Predict binary class labels (0 or 1) based on decision threshold tau.
"""
probs = predict_proba(X, w, b)
return (probs >= threshold).astype(int)
examples/test_logistic_lib.py (2000 bytes)
"""
Tests for reference logistic regression implementation.
"""
import pytest
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
import logistic_lib as logreg
def test_sigmoid_properties():
assert abs(logreg.sigmoid(np.array([0.0]))[0] - 0.5) < 1e-9
assert logreg.sigmoid(np.array([100.0]))[0] > 0.9999
assert logreg.sigmoid(np.array([-100.0]))[0] < 0.0001
z = np.array([-3.5, -1.0, 0.0, 1.2, 4.0])
np.testing.assert_allclose(logreg.sigmoid(-z), 1.0 - logreg.sigmoid(z), atol=1e-9)
def test_binary_cross_entropy_extremes():
y_true = np.array([1.0, 0.0])
y_perfect = np.array([0.99999, 0.00001])
assert logreg.binary_cross_entropy(y_true, y_perfect) < 1e-4
y_mid = np.array([0.5, 0.5])
assert abs(logreg.binary_cross_entropy(y_true, y_mid) - np.log(2.0)) < 1e-4
def test_gradient_descent_convergence():
rng = np.random.default_rng(42)
X = rng.normal(size=(100, 2))
y = ((X[:, 0] * 2.0 - X[:, 1] * 1.5 + 0.5) > 0).astype(float)
w, b, history = logreg.fit_logistic_regression(X, y, lr=0.5, epochs=300)
assert history[-1] < history[0]
assert history[-1] < 0.25
preds = logreg.predict_classes(X, w, b)
acc = np.mean(preds == y)
assert acc >= 0.90
def test_breast_cancer_benchmark():
data = load_breast_cancer()
X = data.data
y = data.target
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
w, b, history = logreg.fit_logistic_regression(X_scaled, y, lr=0.2, epochs=1000)
scratch_preds = logreg.predict_classes(X_scaled, w, b)
scratch_acc = np.mean(scratch_preds == y)
sk_model = LogisticRegression(C=1e9, solver="lbfgs", max_iter=1000)
sk_model.fit(X_scaled, y)
sk_preds = sk_model.predict(X_scaled)
sk_acc = np.mean(sk_preds == y)
assert scratch_acc >= 0.95
assert sk_acc >= 0.95
assert abs(scratch_acc - sk_acc) < 0.03
metadata.yml (796 bytes)
lesson_id: D155
day: 155
kind: classification-fundamentals
languages:
- python
setup_commands:
- python3 -m venv .venv
- .venv/bin/pip install -r requirements/requirements.txt
run_commands:
- .venv/bin/python examples/logistic_lib.py
test_commands:
- ./tests/run_tests.sh
cleanup_commands:
- rm -rf .venv __pycache__ .pytest_cache
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: >-
macOS (Apple Silicon, arm64, CPU only), Python 3.14.0, numpy 2.5.2,
scikit-learn 1.9.0, pytest 9.1.1, scipy 1.15.2 -- bash tests/run_tests.sh -> 4 checks,
0 failure(s), exit 0. pytest examples -v -> 4 passed. pytest starter -v -> 3 passed.
Numerical verification reproduced exact sigmoid midpoint, symmetry, and log-loss at p=0.5.
requirements/requirements.txt (61 bytes)
numpy==2.5.2
scikit-learn==1.9.0
pytest==9.1.1
scipy==1.15.2
starter/logistic_lib.py (1408 bytes)
"""
Logistic Regression library starter implementation.
"""
import numpy as np
def sigmoid(z: np.ndarray) -> np.ndarray:
"""Compute the logistic sigmoid function."""
raise NotImplementedError("Implement sigmoid")
def predict_proba(X: np.ndarray, w: np.ndarray, b: float) -> np.ndarray:
"""Compute predicted probabilities for class 1."""
raise NotImplementedError("Implement predict_proba")
def binary_cross_entropy(y_true: np.ndarray, y_prob: np.ndarray, eps: float = 1e-15) -> float:
"""Compute binary cross-entropy (log loss)."""
raise NotImplementedError("Implement binary_cross_entropy")
def compute_gradients(X: np.ndarray, y_true: np.ndarray, y_prob: np.ndarray) -> tuple[np.ndarray, float]:
"""Compute gradients of loss with respect to w and b."""
raise NotImplementedError("Implement compute_gradients")
def fit_logistic_regression(
X: np.ndarray,
y: np.ndarray,
lr: float = 0.1,
epochs: int = 1000,
tol: float = 1e-6
) -> tuple[np.ndarray, float, list[float]]:
"""Fit logistic regression model using batch gradient descent."""
raise NotImplementedError("Implement fit_logistic_regression")
def predict_classes(X: np.ndarray, w: np.ndarray, b: float, threshold: float = 0.5) -> np.ndarray:
"""Predict binary class labels (0 or 1) based on probability threshold."""
raise NotImplementedError("Implement predict_classes")
starter/test_logistic_lib.py (541 bytes)
"""
Tests for starter logistic regression implementation.
"""
import pytest
import numpy as np
import logistic_lib as logreg
def test_sigmoid_stub():
with pytest.raises(NotImplementedError):
logreg.sigmoid(np.array([0.0]))
def test_predict_proba_stub():
with pytest.raises(NotImplementedError):
logreg.predict_proba(np.zeros((2, 2)), np.zeros(2), 0.0)
def test_binary_cross_entropy_stub():
with pytest.raises(NotImplementedError):
logreg.binary_cross_entropy(np.array([1, 0]), np.array([0.8, 0.2]))
tests/run_tests.sh (2621 bytes)
#!/usr/bin/env bash
# Day 155 lab harness: "Logistic Regression"
set -u
LAB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$LAB_DIR"
PYTHON="${PYTHON:-../../../../.venv-tools/bin/python3}"
PYTEST="${PYTEST:-../../../../.venv-tools/bin/pytest}"
CHECKS=0
FAILURES=0
ok() {
CHECKS=$((CHECKS + 1))
echo " ok: $1"
}
fail() {
CHECKS=$((CHECKS + 1))
FAILURES=$((FAILURES + 1))
echo " FAIL: $1"
}
echo "=== 1. Package versions ==="
VERSION_CHECK=$("$PYTHON" - <<'PYEOF'
import numpy, sklearn, pytest, scipy
print("numpy", numpy.__version__)
print("scikit-learn", sklearn.__version__)
print("pytest", pytest.__version__)
print("scipy", scipy.__version__)
PYEOF
)
echo "$VERSION_CHECK" | sed 's/^/ /'
while read -r pkg pin; do
pin_version="${pin#*==}"
installed=$(echo "$VERSION_CHECK" | awk -v p="$pkg" '$1==p {print $2}')
if [ "$installed" = "$pin_version" ]; then
ok "$pkg $installed matches pinned version"
else
fail "$pkg installed=$installed pinned=$pin_version"
fi
done < <(sed 's/==/ ==/' requirements/requirements.txt)
echo ""
echo "=== 2. Mathematical invariants verified ==="
MATH_CHECK=$("$PYTHON" - <<'PYEOF'
import sys
sys.path.insert(0, "examples")
import numpy as np
import logistic_lib as logreg
s0 = logreg.sigmoid(np.array([0.0]))[0]
assert abs(s0 - 0.5) < 1e-9, f"s(0)={s0}"
z = np.array([-2.5, 0.0, 2.5])
s_pos = logreg.sigmoid(z)
s_neg = logreg.sigmoid(-z)
assert np.allclose(s_neg, 1.0 - s_pos, atol=1e-9), "sigmoid symmetry violated"
y = np.array([1.0, 0.0])
p = np.array([0.5, 0.5])
loss_mid = logreg.binary_cross_entropy(y, p)
assert abs(loss_mid - np.log(2.0)) < 1e-9, f"loss_mid={loss_mid}"
X = np.eye(2)
y_match = np.array([1.0, 0.0])
p_match = np.array([1.0, 0.0])
gw, gb = logreg.compute_gradients(X, y_match, p_match)
assert np.allclose(gw, 0.0) and abs(gb) < 1e-9, "gradients not zero on exact match"
print("MATH_OK")
PYEOF
)
if [ "$MATH_CHECK" = "MATH_OK" ]; then
ok "All mathematical identities verified exactly"
else
fail "Mathematical verification failed: $MATH_CHECK"
fi
echo ""
echo "=== 3. Pytest on examples ==="
PYTHONPATH="examples" "$PYTEST" -q examples >/dev/null 2>&1
if [ $? -eq 0 ]; then
ok "All 4 reference test cases passed in examples/"
else
fail "Reference test suite failed"
fi
echo ""
echo "=== 4. Pytest on starter ==="
PYTHONPATH="starter" "$PYTEST" -q starter >/dev/null 2>&1
if [ $? -eq 0 ]; then
ok "Starter stub tests executed successfully"
else
fail "Starter stub tests failed"
fi
echo ""
echo "Summary: $CHECKS checks, $FAILURES failure(s)"
if [ $FAILURES -eq 0 ]; then
exit 0
else
exit 1
fi
Troubleshooting
Troubleshooting Guide for Day 155
Common Issues
1. Overflow in Sigmoid Computation (exp overflow)
- Symptom:
RuntimeWarning: overflow encountered in expwhen calculating1.0 / (1.0 + np.exp(-z)). - Cause: Large negative values of
zcausenp.exp(-z)to exceed floating point limits (~709.78 in float64). - Fix: Clip
zinto the safe rangenp.clip(z, -500.0, 500.0)or compute numerically stable sigmoid piecewise:np.where(z >= 0, 1.0 / (1.0 + np.exp(-z)), np.exp(z) / (1.0 + np.exp(z))).
2. Log of Zero in Binary Cross-Entropy
- Symptom:
RuntimeWarning: divide by zero encountered in logreturningNaNloss. - Cause: Predicted probability
preaches exactly0.0or1.0. - Fix: Clip probabilities
np.clip(p, 1e-15, 1.0 - 1e-15).
3. Exploding Gradients without Feature Scaling
- Symptom: Loss increases to infinity or oscillates wildly.
- Cause: Features with unscaled large magnitudes produce massive gradient steps.
- Fix: Standardize features using
StandardScaler(zero mean, unit variance) before gradient descent.
Security notes
Security and Privacy Notes for Day 155
- Local Execution: All code runs entirely in offline memory with zero outbound network calls.
- Synthetic & Standard Datasets: The lab uses
sklearn.datasets.load_breast_cancer(a public de-identified dataset) and synthetic vectors. - Filesystem Safety: The lab creates no persistent files outside its own directory.