llm_from_scratch / lessons/09 · the losslesson 10 / 16

Part IV — Pretraining

The loss — cross-entropy & perplexity

Lesson 08 assembled the full GPT and ran the generation loop — but with random weights it emits gibberish, and it has no way of knowing so. It ranks every token, yet nothing tells it that its ranking is wrong. This lesson defines the single scalar that measures wrongness — cross-entropy, the negative log-probability of the true next token — and its readable twin, perplexity. That one number is the entire target of pretraining; everything after this lesson exists to drive it down.

What the last step left broken
The model from lesson 08 is complete in shape and blind in judgment. Feed it "every effort moves" and it produces a length-V distribution over the next token, but that distribution is essentially uniform noise — the true continuation "you" sits at probability ≈ 1/50{,}257 ≈ 0.00002, no better than a coin with 50,257 sides. Worse, the model cannot tell that this is bad. Gradient descent needs a differentiable number that is small when the prediction is good and large when it is wrong; without it there is nothing to backpropagate, and no training can begin.
Linear position
Forced by: a fully-assembled GPT (08) generates tokens but has no notion of "wrong" — it emits a next-token distribution with no yardstick against the token that actually came next.
New idea: the true next token defines a one-hot target; compare it to the model's distribution with cross-entropy, −log p(true next token), averaged over every position. Minimizing that mean is exactly maximum likelihood of the corpus. Its exponential, perplexity = eloss, reads as an "effective branching factor."
Forces next: one loss on one batch is a single measurement, not learning — we must lower this scalar over an entire corpus, across many batches and epochs, without the optimization diverging. Lesson 10 builds the training loop.
The plan
Four moves. (1) See that the target is one-hot on the true next token and measure the gap to the model's distribution with cross-entropy, −log p(correct). (2) Understand why the log is there — it turns a product of per-token likelihoods into a sum, and mean cross-entropy is the negative log-likelihood of the corpus. (3) Convert the loss into perplexity, eloss, and read it as "how many tokens the model is confused among," anchored by the random baseline ppl = V. (4) Assemble the batch loss over all B·T positions and pin down the shapes. Then drag probability mass in the widget and watch the log punish confident mistakes.

1 · The target is one-hot; the model outputs a distribution

At each position the situation is a small classification problem with V classes. The model reads the tokens so far and produces a length-V distribution p — its guess for the next token. The truth is a single token id, the one that actually appears next in the text (this is the free label from lesson 03: the next token is the answer). We can write that truth as a one-hot vector y — all zeros except a 1 in the slot of the correct token.

So we have two distributions over the same V slots: the model's spread-out p, and the truth's razor-thin spike y. A good model puts its mass where the spike is. The natural way to score "how well does p agree with y" is cross-entropy:

H(y, p) = − Σi=1..V yi · log pi

Because y is one-hot, every term drops out except the one where yi = 1 — the correct token. The whole sum collapses to a single, memorable quantity:

CE = − log p(correct token)

That is the entire loss for one position. It only ever looks at the probability the model assigned to the token that actually came next — the other 50,256 numbers are irrelevant to the score (they matter only indirectly, because probabilities sum to 1, so putting mass on the right token means taking it away from the wrong ones). If the model is certain and right — p(correct) = 1 — then −log 1 = 0, a perfect loss. If it is certain and wrong — p(correct) → 0 — then −log p → +∞. The loss lives on [0, ∞) and it is minimized precisely when the model is confident about the truth.

The trap
Cross-entropy uses the probability of the true token only. A model can rank the correct token second, with 49% mass on it, and still take a real loss (−log 0.49 ≈ 0.71) — being "almost right" is not free. Conversely, dumping 99% on a wrong token is catastrophic even though the correct token still has some mass. Never score against the model's argmax or its top-k; score against the one slot the data says is correct.

2 · Why the logarithm — from a product to a sum

The choice of −log is not cosmetic. Consider the whole point of a language model, the autoregressive factorization from lesson 00: the probability the model assigns to an entire sequence is the product of its per-token conditional probabilities.

p(x1 … xT) = ∏t=1..T p(xt | x<t)

"Train the model" means "make the true corpus as likely as possible under the model" — this is maximum likelihood. But a product of thousands of numbers each below 1 underflows to zero in floating point almost immediately, and products are awkward to differentiate. Take the logarithm and the product becomes a sum, which is numerically stable and has clean, additive gradients:

log p(x1 … xT) = Σt=1..T log p(xt | x<t)

Maximizing that sum is the same as maximizing the product (log is monotonically increasing), and by convention we minimize rather than maximize, so we flip the sign. Divide by the number of tokens to get a per-token average, and you have arrived — exactly — at mean cross-entropy:

loss = − (1/T) Σt log p(xt | x<t) = mean of [ − log p(correct) ] over all positions

These two ideas — "cross-entropy" and "negative log-likelihood" — are the same number seen from two directions. Cross-entropy comes from information theory (bits to encode the truth under the model's code); negative log-likelihood comes from statistics (how improbable the data is under the model). In practice the terms are used interchangeably, and PyTorch's cross_entropy computes exactly this quantity in one call. Concretely it fuses three steps you could do by hand — softmax the logits into probabilities, pick out the probability of each target token, then take the negative mean of the logs — into a single, numerically-stable operation.

1Start from the raw logits the LM head emits — one length-V vector per position, unnormalized.
2Softmax turns each logit vector into a probability distribution p that sums to 1.
3Index out p(correct) — the single probability the model gave the true next token at each position.
4Take log of each of those probabilities (all negative, since p ≤ 1).
5Average the log-probabilities over all positions — one scalar.
6Negate it. That non-negative number is the cross-entropy loss.

3 · Perplexity — the loss made readable

A loss of 10.79 is correct but unintuitive. Perplexity fixes that by undoing the logarithm: exponentiate the loss.

perplexity = eloss = exp(cross-entropy)

The payoff is an interpretation in the model's own currency — tokens. Perplexity is the effective branching factor: roughly how many equally-likely options the model feels it is choosing among at each step. A perplexity of 20 means the model is about as uncertain as if it had to guess uniformly among 20 tokens. Lower is better; a perfect model has perplexity 1 (no uncertainty — it always knows the next token).

The clean anchor is a model that has learned nothing. If it spreads its mass uniformly over all V tokens, then p(correct) = 1/V, so the loss is −log(1/V) = log V and the perplexity is elog V = V. Every value below V is progress away from pure ignorance.

Grounding the numbers
For the GPT-2 vocabulary, V = 50{,}257. A freshly-initialized model therefore starts near loss = ln 50{,}257 ≈ 10.82 and perplexity ≈ 50,257 — it is confused among the entire vocabulary. In the book's worked example the untrained model scores loss 10.79 and perplexity ≈ 48,725: "unsure which among ~48,725 tokens to emit." A well-trained small GPT-2 on ordinary English lands in the single-to-low-double digits of perplexity — down from tens of thousands to a handful. That collapse, from V toward 1, is the whole story of pretraining told in one number.

4 · The batch loss — shapes and the one scalar

Training does not happen one position at a time; it happens over a batch of sequences. The LM head produces logits of shape (B, T, V) — for each of B sequences and each of T positions, a length-V score vector. The targets are the inputs shifted one step left (lesson 03), so they have shape (B, T) — one correct token id per position. The loss is the mean cross-entropy over every one of the B·T positions:

# logits : (B, T, V)   raw scores from the LM head
# targets: (B, T)      the true next-token id at each position

logits_flat  = logits.reshape(B*T, V)   # one row per position
targets_flat = targets.reshape(B*T)     # one correct id per position

loss = cross_entropy(logits_flat, targets_flat)   # a single scalar
#   = mean over all B*T positions of  -log softmax(logits)[correct]

Read the flattening as the point: cross-entropy does not care about the sequence structure, only about a bag of (distribution, correct-id) pairs, so we collapse the batch and time axes together into B·T independent classification problems and average their losses. The result is one number. That scalar is the entire training objective. Backpropagation (lesson 10) will push gradients from this single value back into every weight in the network — the embeddings, all L attention blocks, the MLPs, the LM head — nudging each one to make the true next token a little more probable next time.

Why one scalar is enough
It can feel too simple that a 124-million-parameter model is steered by a single number. But that number is a dense summary of every prediction the model made on the batch, and its gradient is a length-124M vector telling each parameter which way to move to lower it. The loss is scalar; the information it carries — via its gradient — is not. This is why we obsessed over keeping the network differentiable and its gradients alive (the √d_k scale, LayerNorm, residuals): all of that was in service of the moment this one scalar gets backpropagated.

5 · Watch the log punish overconfidence

The widget below is a single next-token distribution over an eight-token vocabulary, with one token marked correct (green outline). The slider concentrates or spreads the probability mass: drag it right and the distribution sharpens toward the token you point it at; drag it left and it flattens toward uniform. Point the mass at the correct token and watch the loss dive toward 0 and perplexity toward 1. Point it confidently at a wrong token and the loss explodes — that is the log's steep wall near p(correct) = 0. The dashed lines mark the random-guess baseline (loss = ln V, ppl = V); anything below them is a model that has learned something. The small inset plots the loss curve −log p so you can see the shape of the punishment directly.

Cross-entropy playground
Proves that the log makes the loss brutally asymmetric: confident-and-right drives loss→0 and perplexity→1, but confident-and-wrong sends loss toward infinity. The dashed baseline is a random guess (loss = ln V, ppl = V); to its right you have learned nothing.
p(correct)
loss = −log p
perplexity = eloss
× better than random

Failure modes & checklist

Failure modes

  • Scoring against the argmax, not the target. Cross-entropy is −log p(true token); grading against the model's own top pick makes the loss trivially small and meaningless.
  • Feeding probabilities into cross_entropy. The PyTorch op expects raw logits and softmaxes internally; pre-softmaxing double-applies it and corrupts the gradient. Pass logits.
  • Wrong flatten / axis. Logits (B,T,V) and targets (B,T) must align position-for-position; flatten to (B·T, V) and (B·T) so each row's correct id matches.
  • Reading perplexity without exponentiating. Perplexity is eloss, not the loss; comparing a raw loss of 3 to a perplexity of 20 is comparing different scales.
  • Panicking at loss ≈ 10.8 on step 0. That is exactly ln V — a correctly-initialized model that has learned nothing yet, not a bug.

Checklist

  • Target is the one-hot true next token; loss collapses to −log p(correct).
  • Mean over all B·T positions → a single scalar.
  • Pass logits (not probabilities) to the cross-entropy op.
  • Perplexity = exp(loss); sanity-check that untrained ≈ V.
  • Lower loss ⇔ higher likelihood of the true corpus (maximum likelihood).

Checkpoint

Try it
Leave the target on "you" (the correct token) and drag confidence from left to right: watch loss fall toward 0 and perplexity toward 1 as the green bar grows. Now switch the target to a wrong token ("the") and drag confidence up again — the loss shoots past the dashed baseline toward the top of the chart, because you are now piling mass away from the truth. Find the confidence where p(correct) ≈ 1/8: the loss should sit right on the dashed line, ln 8 ≈ 2.08, and "× better than random" should read ≈ 1.0. That is the moment your toy model knows exactly nothing.

Where this points next

You now have the one number that defines success — and a way to read it as an effective branching factor. But a loss computed on a single batch is just a snapshot; it tells you how wrong the model is right now, not how to make it less wrong. To actually learn, we have to compute this loss batch after batch, take its gradient, step every parameter a little downhill, and repeat — thousands of times, across the whole corpus, while watching a validation split to catch the model memorizing instead of generalizing. And we must do all of that without the optimization exploding. That machinery — the optimizer (AdamW), the epochs, the learning rate as the master knob, and the train/validation loss curves — is lesson 10, The training loop — AdamW, batches, curves.

Takeaway
The loss is cross-entropy = −log p(true next token), averaged over every position — identical to the corpus's negative log-likelihood, so minimizing it is maximum likelihood. The logarithm turns a product of per-token probabilities into a stable sum and punishes confident mistakes without mercy (loss → ∞ as p(correct) → 0). Perplexity = eloss reads that number as an effective branching factor: a random model over V tokens scores loss = ln V and ppl = V (≈ 50,257 for GPT-2), and training collapses that toward single digits. Over a batch the loss is the mean over all B·T positions — logits (B,T,V) against targets (B,T) — a single scalar that is the entire objective of pretraining.

Interview prompts

Companion reads: cs336 · 02 The Transformer frames this same loss as the negative log-likelihood objective; cs336 · 14 Evaluation puts perplexity alongside downstream benchmarks and its limits.