What a language model's loss actually measures: cross-entropy, KL divergence, and perplexity
Why softmax output is a genuine probability distribution, why the cross-entropy gradient collapses to one clean subtraction, and what a perplexity number in a model card actually means.
When a language model predicts the next token, it doesn’t output one answer, it outputs a full probability distribution: a number for every token in its vocabulary, all of them nonnegative, all of them summing to exactly 1. “The cat sat on the ___” might get mat: 0.7, chair: 0.2, floor: 0.08, moon: 0.02, across every token in a 100,000-word vocabulary. Training a language model is adjusting that distribution, over and over, so it puts more weight on whatever token actually came next in real text. Softmax isn’t a probability function by loose analogy, its output genuinely satisfies the definition, nonnegative entries summing to one, which is exactly why it’s the standard way to turn a model’s raw scores into something that can be scored against reality.
Cross-entropy: a penalty derived, not assumed
Say the true next token is “mat,” which means the correct distribution is on “mat” and everywhere else, a one-hot distribution: all belief concentrated on a single outcome. The model assigns “mat” some probability , and you need a number that’s small when is close to and large when is close to . does exactly this: as , , no penalty for a confident correct answer; as , , unboundedly large penalty for being confidently wrong. It isn’t an arbitrary choice among equally good options, it’s the function that makes the gradient below come out clean, which is the actual reason it’s used everywhere rather than some other penalty shape.
For a one-hot true distribution and predicted distribution (softmax’s output), cross-entropy collapses to a single term, since every other entry of is zero:
where is the correct class. In language modeling this is almost always the one-hot case, so cross-entropy reduces to something simpler than the general sum suggests: just of the probability the model assigned to the actual right answer.
The gradient of this loss with respect to the pre-softmax logits is where it gets genuinely elegant. Chaining together the softmax Jacobian from backprop through attention (diagonal terms , off-diagonal terms ) with the derivative of , every messy term cancels, leaving:
Predicted probability minus true probability, entry by entry. This clean cancellation is exactly why softmax and cross-entropy are paired together almost universally rather than mixed and matched with other loss functions: the messy Jacobian terms from one and the messy log-derivative from the other were never going to cancel this cleanly with some other pairing.
Worked example. Vocabulary [mat, chair, floor, moon], true token “mat” so , predicted . Loss: . Gradient: , negative at the correct token (push its probability up), positive everywhere else (push them down). The gradient’s magnitude is quite literally “how wrong was this prediction.”
import numpy as np
def softmax(z):
z = z - np.max(z)
e = np.exp(z)
return e / np.sum(e)
def cross_entropy(y_true_onehot, y_pred):
return -np.log(y_pred[np.argmax(y_true_onehot)])
z = np.array([1.5, 0.5, -0.3, -1.8])
y_true = np.array([1, 0, 0, 0])
y_pred = softmax(z)
print(cross_entropy(y_true, y_pred)) # 0.357...
print(y_pred - y_true) # the clean gradient: [-0.3, 0.2, ...]
Checking this gradient against an independent finite-difference estimate (perturb each logit, measure the loss change, divide) confirms the two agree to about : the cancellation is a real algebraic fact, not a hand-wave.
This is not a toy example scaled down. Cross-entropy between a model’s predicted next-token distribution and the actual next token in the training corpus is the training objective for essentially every pretrained LLM: millions of applications of this exact gradient, one per token, flowing backward through the attention and linear-layer machinery underneath.
KL divergence: the more general object cross-entropy hides inside
KL divergence measures how much probability mass gets “wasted” describing a system actually governed by using an approximation instead. It is not symmetric, in general, and that asymmetry isn’t a flaw, it’s meaningful: penalizing for being confident somewhere isn’t is a different failure than penalizing for being unconfident somewhere is. RLHF exploits this directly. Every standard RLHF objective penalizes a policy model’s KL divergence from a reference model, not the reverse, specifically to control one of those two failure modes and not the other.
Entropy: the floor cross-entropy can never beat
Define the “surprise” of an outcome with probability as , the same function cross-entropy was built from. Entropy is the expected surprise of a distribution under itself:
A coin that’s 100% heads has zero entropy, no surprise is possible. A fair coin has maximum entropy for two outcomes, every flip is a genuine toss-up. For a model finishing “The capital of France is ___,” low entropy is the right answer; for a model at the start of a story, high entropy is.
The relationship between entropy, cross-entropy, and KL divergence is worth stating precisely rather than leaving implicit:
Cross-entropy, the actual training loss, equals the data’s own irreducible uncertainty plus a nonnegative penalty for the model being imperfect. Training drives that KL term toward zero, but it can never push cross-entropy below , no matter how good the model gets, because is a property of the data, not the model. Language has genuine irreducible uncertainty: usually several genuinely plausible next words exist, there isn’t one deterministic right answer, and the entropy term is where that fact lives mathematically. For a one-hot true distribution specifically, and becomes numerically identical to cross-entropy itself, which is exactly why the one-hot case in language modeling makes KL divergence feel almost redundant with cross-entropy: in that case, it is.
def entropy(p, eps=1e-12):
p = np.clip(p, eps, 1.0)
return -np.sum(p * np.log(p))
def kl_divergence(y_true, y_pred, eps=1e-12):
y_true_c, y_pred_c = np.clip(y_true, eps, 1.0), np.clip(y_pred, eps, 1.0)
return np.sum(y_true * np.log(y_true_c / y_pred_c))
H = entropy(y_true.astype(float)) # 0.0, one-hot has no uncertainty
CE = cross_entropy(y_true, y_pred) # 0.357
KL = kl_divergence(y_true.astype(float), y_pred) # 0.357, identical to CE here
print(H, KL, H + KL, CE) # H + KL == CE, exactly
Perplexity: the number everyone actually reports
Perplexity is just entropy re-expressed in a more intuitive unit: (or , matching whatever log base was used). It isn’t a separate concept, it’s a unit conversion, chosen because “the model was as uncertain as if guessing uniformly among 20 options” lands more intuitively than “cross-entropy was 3.0 nats,” even though the two numbers carry identical information. When a paper reports “perplexity 15 on this benchmark,” that’s the average cross-entropy over a large held-out corpus, exponentiated back out of log-space; lower means the model was less surprised by real text on average, and it’s the standard cheap sanity check run before any more expensive downstream evaluation.
| Concept | Question it answers | Formula |
|---|---|---|
| Entropy | How uncertain is this one distribution, on its own? | |
| Cross-entropy | How surprised is a predicted distribution by reality? | |
| KL divergence | How much of that surprise is the model’s fault, beyond irreducible entropy? | |
| Perplexity | Cross-entropy, rescaled to “effective number of equally likely options” | or |
| Mutual information | How much does knowing reduce uncertainty about ? |
Mutual information, , is the one entry in that table worth a separate word because it’s easy to invoke loosely. If tells you nothing about , and ; if fully determines , and , the maximum possible reduction. Papers and blog posts that say an attention head “encodes information about” something are sometimes making this precise, computed claim, and sometimes just speaking loosely; the two get blurred constantly, and it’s worth noticing which one you’re actually reading.
Common mistakes
Treating softmax’s output as merely “probability-like” instead of a genuine probability distribution: the whole reason cross-entropy applies to it at all is that it satisfies the actual mathematical definition, not an approximation of one.
Assuming a well-trained model’s cross-entropy should approach zero: it approaches , the data’s own irreducible entropy, which is nonzero for essentially all real language. A loss that’s stopped dropping isn’t necessarily a broken run, it might be near the actual floor.
Letting underflow to exactly in low-precision floating point, sending to infinity or NaN. This is the same range-limit failure mode as everywhere else in this series, and the standard fix, a small epsilon before the log or working in log-space throughout, exists because this is common enough to need one.
Using “information” in the loose, intuitive sense and the rigorous mutual-information sense interchangeably: they aren’t the same claim, and conflating them is a real, documented source of imprecise statements in interpretability writing.
Try it yourself
Beginner. For with true token at index 1, compute cross-entropy by hand, then the gradient , and check that the loss isn’t tiny (0.3 isn’t a confident correct prediction).
Intermediate. Construct a soft label, , representing genuine ambiguity in the true answer rather than one-hot certainty. Show in this case, and verify still holds.
Advanced. Derive the result from scratch: combine the softmax Jacobian’s diagonal and off-diagonal terms with the derivative of with respect to , and show the messy pieces cancel into the clean subtraction, rather than trusting the finite-difference check above as sufficient proof.
The one-sentence version: a model’s output is a genuine probability distribution, cross-entropy measures how surprised that distribution is by the actual next token in a way that happens to produce a remarkably clean gradient, , and every number derived from it, KL divergence, entropy, perplexity, is a different way of asking the same question: how much of that surprise is irreducible in the data itself, and how much of it is the model’s own fault. That gradient is also exactly what gradient descent and Adam consume on every single training step, millions of times over.