AI-210 · Lesson 12 of 12 · 29 min

Putting it together: models from first principles

This final lesson uses the mathematics of the whole course on two complete models built from scratch. Logistic regression is the simplest probabilistic classifier, and deriving it touches nearly every topic: a probability model (Lesson 6), its likelihood and the cross-entropy loss (Lesson 9), gradients and Hessians (Lesson 4), convexity and Newton's method (Lesson 11), numerical stability (Lesson 5) and evaluation (Lesson 8). A two-layer neural network then shows what changes when the model is no longer linear in its parameters. The aim is to see that the machinery of modern machine learning is not a collection of tricks but a few ideas used consistently.

Step 1: a probability model

For binary labels \(y \in \{0, 1\}\) and features \(\mathbf{x}\), logistic regression models the probability of the positive class as

\[p(y = 1 \mid \mathbf{x}) = \sigma(\mathbf{w}^{T}\mathbf{x}) = \frac{1}{1 + e^{-\mathbf{w}^{T}\mathbf{x}}}\]

where \(\mathbf{x}\) includes a constant 1 for the intercept. The score \(z = \mathbf{w}^{T}\mathbf{x}\) is the log-odds: \(\log\frac{p}{1-p} = z\), so each weight says how much its feature raises the log-odds. The decision boundary \(z = 0\) is a hyperplane, which is why logistic regression is a linear classifier.

Step 2: the likelihood and the loss

Each label is a Bernoulli outcome with probability \(p_{i} = \sigma(z_{i})\). The log-likelihood of the data is \(\sum_{i}[y_{i}\log p_{i} + (1 - y_{i})\log(1 - p_{i})]\). Maximizing it (maximum likelihood, Lesson 6) is the same as minimizing its negative, which is the binary cross-entropy between the labels and the predictions (Lesson 9):

\[\mathcal{L}(\mathbf{w}) = \sum_{i=1}^{n}\left[\log(1 + e^{z_{i}}) - y_{i}z_{i}\right], \qquad z_{i} = \mathbf{w}^{T}\mathbf{x}_{i}\]

The second form, obtained by substituting \(\sigma\) and simplifying, is the one to compute: \(\log(1 + e^{z})\), evaluated with a stable function such as numpy.logaddexp(0, z), never overflows (Lesson 5).

Step 3: gradient and Hessian

Differentiating with the chain rule, using \(\sigma^{\prime}(z) = \sigma(z)(1 - \sigma(z))\), gives remarkably simple results:

\[\nabla\mathcal{L} = \sum_{i}(p_{i} - y_{i})\mathbf{x}_{i} = \mathbf{X}^{T}(\mathbf{p} - \mathbf{y}), \qquad \mathbf{H} = \mathbf{X}^{T}\mathbf{S}\mathbf{X}, \quad \mathbf{S} = \text{diag}\big(p_{i}(1 - p_{i})\big)\]

The gradient is the prediction error weighted by the features, the same form as for least squares. The Hessian is \(\mathbf{X}^{T}\mathbf{S}\mathbf{X}\) with positive weights, so for any vector \(\mathbf{v}\), \(\mathbf{v}^{T}\mathbf{H}\mathbf{v} = \sum_{i}s_{i}(\mathbf{x}_{i}^{T}\mathbf{v})^{2} \ge 0\): the Hessian is positive semidefinite everywhere and the loss is convex (Lesson 11). There are no bad local minima.

Checking the analytic gradient against finite differences, and confirming the Hessian is positive definite.
import numpy as np

rng = np.random.default_rng(18)
n = 400
X = rng.normal(size=(n, 2))
w_true, b_true = np.array([2.0, -1.0]), 0.5
p_true = 1 / (1 + np.exp(-(X @ w_true + b_true)))
y = (rng.random(n) < p_true).astype(float)
Xb = np.column_stack([np.ones(n), X])                     # add an intercept column
sigmoid = lambda z: 1 / (1 + np.exp(-z))

def nll(w):                                               # negative log-likelihood (cross-entropy)
    z = Xb @ w
    return np.sum(np.logaddexp(0, z) - y * z)             # stable form of -log likelihood

w = np.array([0.2, 1.0, -0.5])
p = sigmoid(Xb @ w)
g = Xb.T @ (p - y)
H = Xb.T @ (Xb * (p * (1 - p))[:, None])

h = 1e-5
g_num = np.array([(nll(w + h * e) - nll(w - h * e)) / (2 * h) for e in np.eye(3)])
print("Analytic gradient: ", np.round(g, 4))
print("Numerical gradient:", np.round(g_num, 4))
print("Hessian eigenvalues:", np.round(np.linalg.eigvalsh(H), 3), "-> all positive: the problem is convex")
Output
Analytic gradient:  [ -5.784  -35.8724   5.3455]
Numerical gradient: [ -5.784  -35.8724   5.3455]
Hessian eigenvalues: [51.501 77.676 86.01 ] -> all positive: the problem is convex

Step 4: optimization

Because the loss is convex and smooth, both gradient descent and Newton's method find the global minimum. Gradient descent takes small steps along the negative gradient; Newton's method uses the Hessian to jump to the minimum of a local quadratic model, \(\mathbf{w} \leftarrow \mathbf{w} - \mathbf{H}^{-1}\nabla\mathcal{L}\). For logistic regression, each Newton step is a weighted least-squares problem, so the method is also called iteratively reweighted least squares (IRLS).

Fitting logistic regression by gradient descent and by Newton's method (IRLS), from data generated with known weights.
import numpy as np

rng = np.random.default_rng(18)
n = 400
X = rng.normal(size=(n, 2))
w_true, b_true = np.array([2.0, -1.0]), 0.5
p_true = 1 / (1 + np.exp(-(X @ w_true + b_true)))
y = (rng.random(n) < p_true).astype(float)
Xb = np.column_stack([np.ones(n), X])                     # add an intercept column
sigmoid = lambda z: 1 / (1 + np.exp(-z))

def nll(w):                                               # negative log-likelihood (cross-entropy)
    z = Xb @ w
    return np.sum(np.logaddexp(0, z) - y * z)             # stable form of -log likelihood

w = np.zeros(3)
for step in range(1, 2001):                               # gradient descent
    grad = Xb.T @ (sigmoid(Xb @ w) - y)
    w -= 0.01 * grad
    if step in (10, 100, 2000):
        print(f"GD step {step:>4}: NLL {nll(w):.4f}, w = {np.round(w, 3)}")

w = np.zeros(3)
for it in range(1, 7):                                    # Newton method (IRLS)
    p = sigmoid(Xb @ w)
    H = Xb.T @ (Xb * (p * (1 - p))[:, None])
    w -= np.linalg.solve(H, Xb.T @ (p - y))
    print(f"Newton iteration {it}: NLL {nll(w):.6f}")
print("Newton solution:", np.round(w, 3), "| true (b, w1, w2): [0.5, 2.0, -1.0]")
Output
GD step   10: NLL 174.9172, w = [ 0.402  1.961 -0.735]
GD step  100: NLL 174.8455, w = [ 0.421  2.039 -0.759]
GD step 2000: NLL 174.8455, w = [ 0.421  2.039 -0.759]
Newton iteration 1: NLL 187.252710
Newton iteration 2: NLL 175.933003
Newton iteration 3: NLL 174.860961
Newton iteration 4: NLL 174.845531
Newton iteration 5: NLL 174.845527
Newton iteration 6: NLL 174.845527
Newton solution: [ 0.421  2.039 -0.759] | true (b, w1, w2): [0.5, 2.0, -1.0]

Newton's method converges to six-figure accuracy in five iterations, showing its quadratic convergence near the optimum; gradient descent takes about a hundred steps to reach the same loss. Both recover weights close to the true values (0.5, 2.0, −1.0), with the differences due to the randomness of 400 samples. For millions of features, the \(d \times d\) Hessian becomes too expensive and first-order methods win; for a few hundred features, Newton's method is hard to beat.

Worked problem

One Newton step by hand

A one-parameter model with no intercept, \(p = \sigma(wx)\), has two examples: \((x, y) = (1, 1)\) and \((-1, 0)\). Starting from \(w = 0\), take one Newton step.

Show solution
  1. At \(w = 0\), both \(p_{i} = 0.5\).
  2. Gradient: \((0.5 - 1)(1) + (0.5 - 0)(-1) = -0.5 - 0.5 = -1\).
  3. Hessian: \(0.25 \times 1^{2} + 0.25 \times (-1)^{2} = 0.5\).
  4. Step: \(w = 0 - (-1)/0.5 = 2\).

Answer: \(w = 2\), giving \(p = \sigma(2) = 0.88\) for the positive example. Because these two points are perfectly separable, the true maximum likelihood estimate is \(w \to \infty\) and further Newton steps keep increasing \(w\); regularization (a Gaussian prior, Lesson 6) is needed to obtain a finite answer. Separable data is a classic trap for unregularized logistic regression.

Step 5: evaluation and interpretation

A probabilistic model should be judged not only on accuracy but on the quality of its probabilities. Cross-entropy on held-out data measures how much the model reduces uncertainty compared with a baseline that ignores the features; the difference is the information the features provide, in nats per example (Lesson 9). Calibration asks whether predicted probabilities match observed frequencies: among examples predicted at about 70%, about 70% should be positive.

Cross-entropy compared with a base-rate predictor, and a calibration table.
import numpy as np

rng = np.random.default_rng(18)
n = 400
X = rng.normal(size=(n, 2))
w_true, b_true = np.array([2.0, -1.0]), 0.5
p_true = 1 / (1 + np.exp(-(X @ w_true + b_true)))
y = (rng.random(n) < p_true).astype(float)
Xb = np.column_stack([np.ones(n), X])                     # add an intercept column
sigmoid = lambda z: 1 / (1 + np.exp(-z))

def nll(w):                                               # negative log-likelihood (cross-entropy)
    z = Xb @ w
    return np.sum(np.logaddexp(0, z) - y * z)             # stable form of -log likelihood

w = np.zeros(3)
for _ in range(8):
    p = sigmoid(Xb @ w)
    w -= np.linalg.solve(Xb.T @ (Xb * (p * (1 - p))[:, None]), Xb.T @ (p - y))
p = sigmoid(Xb @ w)

ce = nll(w) / n
base = y.mean()
ce_base = -(base * np.log(base) + (1 - base) * np.log(1 - base))
print(f"Cross-entropy per example: model {ce:.3f} nats, predict-the-base-rate {ce_base:.3f} nats")
print(f"Information gained from the features: {ce_base - ce:.3f} nats per example")

bins = np.digitize(p, [0.2, 0.4, 0.6, 0.8])
print("Calibration (predicted vs observed rate of y = 1 by bin):")
for b in range(5):
    m = bins == b
    print(f"  bin {b}: n = {m.sum():>3}, predicted {p[m].mean():.2f}, observed {y[m].mean():.2f}")
Output
Cross-entropy per example: model 0.437 nats, predict-the-base-rate 0.688 nats
Information gained from the features: 0.251 nats per example
Calibration (predicted vs observed rate of y = 1 by bin):
  bin 0: n =  87, predicted 0.10, observed 0.06
  bin 1: n =  55, predicted 0.29, observed 0.38
  bin 2: n =  63, predicted 0.51, observed 0.54
  bin 3: n =  73, predicted 0.70, observed 0.67
  bin 4: n = 122, predicted 0.93, observed 0.92

The features reduce the cross-entropy from 0.688 to 0.437 nats per example, about 0.25 nats of information each. The calibration table shows predicted and observed rates agreeing to within sampling error in every bin, as expected for a correctly specified model fitted by maximum likelihood. (Evaluated on its own training data, as here for brevity, the agreement is slightly flattering; Lesson 8 explains why a held-out set is needed for an honest assessment.)

Two panels showing the two-moons data set of interleaved crescent-shaped classes in blue and gold. In the left panel, the logistic regression decision boundary is a straight line that cuts through both crescents, misclassifying the tips. In the right panel, the two-layer network's decision boundary is a smooth curve that follows the gap between the crescents.
A linear model draws a straight boundary; a two-layer network learns a curved one.

Step 6: beyond linear models

When the classes cannot be separated by a hyperplane, logistic regression on the raw features fails however well it is optimized: its bias is the problem (Lesson 8). A neural network replaces the fixed features with learned ones. A two-layer network computes hidden features \(\mathbf{a} = \tanh(\mathbf{W}_{1}^{T}\mathbf{x} + \mathbf{b}_{1})\) and then performs logistic regression on them: \(p = \sigma(\mathbf{w}_{2}^{T}\mathbf{a} + b_{2})\). Training uses the same cross-entropy loss, with gradients from backpropagation (Lessons 4 and 5). The output layer's gradient is again "prediction minus label" times the hidden features; it is then passed back through \(\mathbf{w}_{2}\) and the derivative of tanh, \(1 - a^{2}\), to the first layer.

A two-layer network with 16 tanh units, trained from scratch with backpropagation and weight decay, compared with logistic regression on the two-moons data.
import numpy as np

rng = np.random.default_rng(19)
def moons(n):
    t = rng.uniform(0, np.pi, n)
    top = np.column_stack([np.cos(t), np.sin(t)])
    bottom = np.column_stack([1 - np.cos(t), 0.5 - np.sin(t)])
    X = np.vstack([top, bottom]) + rng.normal(0, 0.15, (2 * n, 2))
    return X, np.r_[np.zeros(n), np.ones(n)]
Xtr, ytr = moons(200)
Xte, yte = moons(500)

H = 16
W1 = rng.normal(0, 1, (2, H)); b1 = np.zeros(H)
W2 = rng.normal(0, 1 / np.sqrt(H), H); b2 = 0.0
lr, lam = 0.5, 1e-3
for epoch in range(3000):
    Z = Xtr @ W1 + b1; A = np.tanh(Z)                    # forward
    p = 1 / (1 + np.exp(-(A @ W2 + b2)))
    d_out = (p - ytr) / ytr.size                        # backward: gradient of mean cross-entropy
    gW2 = A.T @ d_out + lam * W2; gb2 = d_out.sum()
    dA = np.outer(d_out, W2) * (1 - A**2)
    gW1 = Xtr.T @ dA + lam * W1; gb1 = dA.sum(axis=0)
    W1 -= lr * gW1; b1 -= lr * gb1; W2 -= lr * gW2; b2 -= lr * gb2

def acc(X, y):
    p = 1 / (1 + np.exp(-(np.tanh(X @ W1 + b1) @ W2 + b2)))
    return np.mean((p > 0.5) == y)

Xb = np.column_stack([np.ones(len(Xtr)), Xtr]); w = np.zeros(3)
for _ in range(5000):                                   # logistic regression baseline
    w -= 0.5 * Xb.T @ (1 / (1 + np.exp(-Xb @ w)) - ytr) / len(ytr)
lin_acc = np.mean(((np.column_stack([np.ones(len(Xte)), Xte]) @ w) > 0) == yte)
print(f"Logistic regression test accuracy: {lin_acc:.3f}")
print(f"Two-layer network: train accuracy {acc(Xtr, ytr):.3f}, test accuracy {acc(Xte, yte):.3f}")
Output
Logistic regression test accuracy: 0.866
Two-layer network: train accuracy 0.993, test accuracy 0.990

The network reaches 99% test accuracy where the linear model is stuck near 87%. What the network gains in flexibility it loses in guarantees: its loss is no longer convex in the parameters, different random initializations give different solutions, and the step size, width, regularization and number of epochs must be chosen by validation rather than by theory. Those are the trade-offs studied in the courses that follow: optimization for machine learning (AI-310), probabilistic models (AI-320), and the deep learning of language (AI-340) and vision (AI-350).

Worked problem

Counting the network's parameters

How many parameters does the two-layer network above have, with 2 inputs, 16 hidden units and 1 output? How many does logistic regression have?

Show solution
  1. First layer: \(2 \times 16\) weights + 16 biases = 48.
  2. Second layer: 16 weights + 1 bias = 17.
  3. Total: 65. Logistic regression: 2 weights + 1 bias = 3.

Answer: 65 parameters against 3. With 400 training points, the network is well within the classical regime (Lesson 8): it has enough capacity to bend its boundary but not enough to memorize noise, and the small weight-decay term keeps it smooth.

The common thread

Every model in this course, and nearly every model in modern AI, is built the same way: choose a parameterized family of functions (linear algebra), turn the data into a likelihood or loss (probability and information theory), compute its gradient (calculus and automatic differentiation), minimize it (optimization), and measure how well the result generalizes (learning theory). Large language models replace the two-moons data with trillions of tokens and the 65 parameters with hundreds of billions, but the cross-entropy loss, the backpropagated gradients, the low-precision matrix products and the held-out evaluations are the same mathematics you have now worked through.

Common mistakes

  • MistakeComputing log(σ(z)) directly.

    InsteadUse the stable form log(1 + e^z) − yz with logaddexp.

  • MistakeFitting unregularized logistic regression to separable data.

    InsteadAdd a penalty (prior) so the weights stay finite.

  • MistakeJudging a probabilistic classifier by accuracy alone.

    InsteadCheck held-out cross-entropy and calibration too.

  • MistakeAssuming a neural network's training finds the global minimum.

    InsteadIts loss is non-convex; compare several runs and validate.

  • MistakeSkipping the gradient check when writing backpropagation by hand.

    InsteadCompare with finite differences before training.

Summary and key terms

  • Logistic regression models log-odds as linear in the features: p = σ(wᵀx).
  • Maximum likelihood equals minimizing binary cross-entropy, computed stably.
  • The gradient is Xᵀ(p − y) and the Hessian XᵀSX is positive semidefinite, so the loss is convex.
  • Newton's method (IRLS) converges in a few iterations; gradient descent needs many more.
  • Evaluate probabilities with held-out cross-entropy and calibration.
  • A two-layer network learns its own features, gaining flexibility and losing convexity.
  • Linear algebra, probability, calculus, optimization and learning theory combine in every model.
Logistic regression
A linear model of the log-odds of a binary outcome.
Log-odds
log(p/(1 − p)).
Sigmoid
σ(z) = 1/(1 + e^−z).
Bernoulli likelihood
The probability of binary outcomes under predicted probabilities.
IRLS
Iteratively reweighted least squares: Newton's method for logistic regression.
Quadratic convergence
Error roughly squaring at each iteration near the optimum.
Separable data
Data that a hyperplane classifies perfectly.
Calibration
Agreement between predicted probabilities and observed frequencies.
Hidden layer
An intermediate layer that computes learned features.
Weight decay
An L2 penalty on weights, equivalent to a Gaussian prior.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. If z = 0.8, what are p and the odds?

    Show answer

    p = σ(0.8) ≈ 0.69; odds = e^0.8 ≈ 2.23.

  2. A weight of 0.7 on a feature: effect of +1 in that feature on the odds?

    Show answer

    Multiplies the odds by e^0.7 ≈ 2.01.

  3. Write the logistic regression gradient in matrix form.

    Show answer

    Xᵀ(p − y).

  4. Why is the logistic regression loss convex?

    Show answer

    Its Hessian XᵀSX has nonnegative weights, so it is positive semidefinite.

  5. Why does Newton's method need few iterations here?

    Show answer

    It converges quadratically near the optimum of a smooth convex function.

  6. What goes wrong with separable data?

    Show answer

    The likelihood keeps increasing as weights grow; the MLE is infinite.

  7. Base rate 0.3. Cross-entropy of predicting 0.3 always?

    Show answer

    −(0.3 ln 0.3 + 0.7 ln 0.7) ≈ 0.611 nats.

  8. In a calibrated model, of 200 examples predicted at 0.4, about how many are positive?

    Show answer

    About 80.

  9. Parameters in a network with 10 inputs, 32 hidden units and 1 output?

    Show answer

    10 × 32 + 32 + 32 + 1 = 385.

  10. Name one thing lost when moving from logistic regression to a neural network.

    Show answer

    Convexity (guaranteed global optimum), or interpretability.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz