04 · Logistic Regression Gradient from Scratch¶
Logistic regression is the cleanest place to see MLE, the chain rule, and convexity all fit together into one working classifier — derived here completely from scratch, gradient and Hessian.
Model¶
Loss: negative log-likelihood of a Bernoulli (Level 3 Module 05)¶
For \(n\) examples:
Gradient¶
Using the fused sigmoid+cross-entropy result from Level 3 Module 06, \(\partial L_i/\partial z_i = \hat y_i - y_i\) where \(z_i=w^\top x_i+b\). Chaining through \(z_i = w^\top x_i + b\):
This has the same clean form as linear regression's gradient (\(X^\top(\hat y - y)\)) — only the definition of \(\hat y\) (sigmoid vs. identity) differs.
Hessian: proof of convexity¶
Differentiating the gradient once more (chain rule, using \(\hat y_i(1-\hat y_i)\) from Level 3 Module 01):
Since \(\hat y_i(1-\hat y_i) > 0\) for all \(\hat y_i \in (0,1)\), \(S\) is positive definite, so \(\nabla_w^2 L = X^\top S X\) is positive semi-definite for any \(X\) — proving (Level 4 Module 01) that logistic regression's loss is convex: gradient descent always finds the global optimum.
Worked numeric example¶
\(x=(1,2), y=1, w=(0.1,-0.2), b=0\).
Numeric verification¶
import numpy as np
def sigmoid(z):
return 1 / (1 + np.exp(-z))
x = np.array([1.0, 2.0])
y = 1.0
w = np.array([0.1, -0.2])
b = 0.0
z = w @ x + b
y_hat = sigmoid(z)
grad_w = (y_hat - y) * x
grad_b = y_hat - y
print(f"z={z:.4f} y_hat={y_hat:.4f}")
print(f"analytic grad_w={grad_w} grad_b={grad_b:.4f}")
def loss(w, b):
z = w @ x + b
yh = sigmoid(z)
return -(y*np.log(yh) + (1-y)*np.log(1-yh))
h = 1e-6
grad_w_num = np.array([
(loss(w + np.array([h,0]), b) - loss(w - np.array([h,0]), b)) / (2*h),
(loss(w + np.array([0,h]), b) - loss(w - np.array([0,h]), b)) / (2*h),
])
grad_b_num = (loss(w, b+h) - loss(w, b-h)) / (2*h)
print(f"numeric grad_w={grad_w_num} grad_b={grad_b_num:.4f}")
# Hessian PSD check (single example, 2 features)
S = y_hat * (1 - y_hat)
X = x.reshape(1, -1)
H = S * (X.T @ X)
print(f"Hessian=\n{H}\neigenvalues={np.linalg.eigvalsh(H)} (should be >= 0)")
z=-0.3000 y_hat=0.4256
analytic grad_w=[-0.5744 -1.1488] grad_b=-0.5744
numeric grad_w=[-0.5744 -1.1488] grad_b=-0.5744
Hessian=
[[0.2445 0.489 ]
[0.489 0.978 ]]
eigenvalues=[0. 1.2225] (should be >= 0)
How It Actually Works¶
The sigmoid function, \(\sigma(z) = 1/(1+e^{-z})\), computed exactly as
written overflows for very negative \(z\): exp(-z) for z = -1000 returns
inf, giving 1/(1+inf) = 0.0 — which is actually the mathematically
correct limit, just reached via an intermediate overflow rather than
smoothly. The more dangerous case is the paired log-loss computation,
\(-\left[y\log\sigma(z) + (1-y)\log(1-\sigma(z))\right]\): if \(\sigma(z)\)
has already rounded to exactly 0.0 or 1.0 in floating point (which
happens for any \(|z|\) larger than about 36 in float64), the subsequent
log(0) returns -inf, and if that term is then multiplied by y=0
elsewhere in the formula, you get 0 * -inf = NaN.
Every production implementation (scikit-learn, PyTorch's
BCEWithLogitsLoss) sidesteps this by computing loss directly from the
raw logit \(z\) rather than from \(\sigma(z)\), using the mathematically
equivalent but numerically safe identity
\(-\left[yz - \log(1+e^{z})\right]\), further rewritten with the
log-sum-exp trick to stay stable for both large positive and large
negative \(z\):
This guarantees the argument to the final exp is always \(\leq 0\)
(so it can never overflow), which is exactly why every framework's
BCEWithLogitsLoss/binary_cross_entropy_with_logits function explicitly
warns against manually chaining a separate sigmoid() and log_loss() —
mathematically identical, numerically much less safe.
Exercise¶
- Implement full-batch gradient descent for logistic regression on a
small synthetic 2D dataset (e.g.
sklearn.datasets.make_classification) and verify the decision boundary visually. - Add L2 regularization (Level 3 Module 04) to the gradient and Hessian; show the regularized Hessian \(\frac{1}{n}X^\top SX + 2\lambda I\) is strictly positive definite even when \(X^\top SX\) alone is only PSD.
- Implement one step of Newton's method (\(w \leftarrow w - H^{-1}\nabla_w L\)) and compare its convergence speed to gradient descent on the same toy dataset.