Cross-entropy and KL divergence
The loss function language models are trained on.
Create a free account Try part of it here
Why it matters
Every chatbot you've used was first trained by making one number smaller. That number is cross-entropy. When you read "the model reached a loss of 1.8" or "perplexity 6", this is what is being measured. Its close cousin, KL divergence, measures how far one set of predictions is from another. It keeps a model from drifting too far from the original during fine-tuning with human feedback (RLHF). It also measures how well a small model copies a big one, which is called distillation. After this lesson Phase 2 is complete: you have the math to read how models are trained.
What you'll be able to do
- Compute cross-entropy between a true distribution and a model's prediction
- Show that with one correct answer, cross-entropy is −log of the probability given to it
- Compute KL divergence and explain it as the extra cost of using the wrong distribution
- Turn an average loss into perplexity
What it covers
- Grading a weather forecast
- With one right answer, it's −log q
- How the loss steers training
- KL divergence: the extra cost
- Perplexity: the loss as a number of choices
- Worked example: perplexity of a short text
Grading a weather forecast
Picture a town where it rains on exactly half the days. Two forecasters work there. Forecaster A says "50% chance of rain" every day. Forecaster B says "10% chance of rain" every day. You want a fair way to score them.
Use surprise, from the entropy lesson. When something happens that you gave probability , your surprise is . An event you called likely costs you little. An event you called unlikely costs you a lot.
In this lesson the log is the natural log: ln on a calculator and math.log in Python. Surprise measured this way comes in nats instead of bits. It's the same idea on a different scale: 1 bit is about 0.693 nats.
Now score forecaster B. On sunny days B had said 90% sun, so the surprise is −ln 0.9 ≈ 0.105 nats. On rainy days B had said 10% rain, so the surprise is −ln 0.1 ≈ 2.303 nats. Each kind of day happens half the time, so B's average is:
0.5 × 0.105 + 0.5 × 2.303 ≈ 1.204 nats
Forecaster A is surprised by −ln 0.5 ≈ 0.693 nats every day. A wins, because A's forecast matches the real weather.
Cross-entropy: average surprise from a forecast
That average is called cross-entropy. Two lists of probabilities go into it. The first, , says how often each outcome really happens. For the town, is [0.5, 0.5].
The second, , is what you predicted. For forecaster B, is [0.9, 0.1].
Here counts through the outcomes: sun, then rain. The sign (sigma) means "add up over every outcome". and are the real and predicted chances of outcome .
In plain words: for each outcome, take how often it really happens. Multiply by how surprised your forecast is when it does. Add those up. That's your average surprise.
In Python the is a for loop. This is forecaster B again:
import math p = [0.5, 0.5] # how often: sun, rain q = [0.9, 0.1] # forecaster B's prediction total = 0 for p_i, q_i in zip(p, q): total += p_i * -math.log(q_i) print(round(total, 3))
It prints 1.204, as you worked out by hand.
Cross-entropy is smallest when the forecast matches reality, . Then it equals the entropy of itself. Any other forecast costs more. If gives a low chance to something that happens often, cross-entropy gets large.
Show the answer
Higher: only a 50/50 forecast reaches the minimum
Cross-entropy is smallest when the forecast matches reality. C's is 0.5 × −ln 0.7 + 0.5 × −ln 0.3 ≈ 0.78 nats, more than A's 0.693.
With one right answer, it's −log q
Training a language model works the same way, with one twist. The model reads some text and predicts the next word. Its prediction gives a chance to every word it knows. Then the real next word is revealed.
This time reality is one answer, not "half the days". So puts all its probability, 1, on the correct word, and 0 on every other word. A list like that, a single 1 and the rest 0s, is called a one-hot vector.
Every term in the sum where drops out. Only the correct word is left:
In plain words: the loss for one prediction is minus the log of the chance the model gave to the right answer.
Say the model gives the word "great" a chance of 0.6, and "great" really does come next. The loss is −ln 0.6 ≈ 0.511 nats.
This is the negative log-likelihood from the maximum likelihood lesson. Models read text in small pieces called tokens. Average this loss over every token in the training text, and you have the language-modeling loss.
import numpy as np vocab = ["great", "good", "okay", "bad"] q = np.array([0.6, 0.25, 0.1, 0.05]) # the model's prediction for correct in ["great", "okay", "bad"]: loss = -np.log(q[vocab.index(correct)]) print(f"if the next word is '{correct}': loss = {loss:.3f} nats")
The losses are 0.511, 2.303 and 2.996 nats. Being confidently right costs little. Being confidently wrong costs a lot. As gets close to 0, the loss shoots up toward infinity. Models are punished hard for being sure of the wrong thing.
import math prob_of_right = 1.0 print(-math.log(prob_of_right) + 0.0)
Show the answer
0.0
log(1.0) is 0, so −log(1.0) comes to 0, and adding 0.0 makes it print as 0.0. If the model gave the right answer 100%, there's no surprise at all, so the loss is zero.
Pick which word actually came next. Raise its score and the loss falls toward 0; give probability to the wrong words and it climbs. Training nudges the logits exactly this way.
Show the answer
About 4.6
−ln(0.01) ≈ 4.6 nats: being confidently wrong is expensive.
How the loss steers training
A model doesn't output chances directly. First it gives each word a raw score, called a logit. Softmax, from the softmax lesson, turns those scores into chances. Training then nudges each score so the loss goes down.
To nudge well, training needs each score's slope. The slope says how much the loss changes when that score goes up a little. Write for the score of word . Write for the loss. When softmax feeds cross-entropy, the slope has a famously simple form:
The curly d, , is said "partial". It means a slope with every other score held still.
In plain words: each score's slope is the predicted chance minus the target. The target is 1 for the right word and 0 for every other word.
Use the same prediction as before, with "great" as the right word:
- "great": 0.6 − 1 = −0.4. The slope is negative, so raising this score lowers the loss. Training pushes it up.
- "good": 0.25 − 0 = 0.25. The slope is positive, so training pushes this score down.
- "okay" and "bad": 0.1 and 0.05. They get pushed down a little.
Each push is scaled by a small step size, so scores move a little at a time. A wrong word the model gave 0.3 would have slope 0.3. The surer the model was of a wrong word, the harder that word's score is pushed down.
q = [0.6, 0.25, 0.1, 0.05] # predicted: great, good, okay, bad p = [1, 0, 0, 0] # target: "great" was right slopes = [q_i - p_i for q_i, p_i in zip(q, p)] print([round(s, 2) for s in slopes])
It prints [-0.4, 0.25, 0.1, 0.05]. The slopes add up to 0, because both lists add up to 1. Every training step of a language model does this at every position in every piece of text.
Go deeper Where q − p comes from
Call the correct word . Softmax gives it the chance , where runs over every word. So the loss is:
Read it as: minus the correct word's score, plus the log of the softmax total.
Now take the slope with respect to one score, . The first part gives −1 when is the correct word, and 0 otherwise. That's . The second part gives , which is softmax again: . Together they make .
KL divergence: the extra cost
Back to the town. Forecaster B averaged 1.204 nats of surprise. The best possible forecast averaged 0.693 nats. So B pays an extra 0.511 nats a day for using the wrong forecast.
That extra cost is the Kullback–Leibler divergence, or KL divergence for short. It's named after the two people who introduced it.
Here is the entropy of : the surprise of the best forecast. The double bar only separates the two lists. The first list, , is reality. The second, , is the forecast.
In plain words: KL divergence is your cross-entropy minus the best one possible. It's the extra surprise you pay for forecasting when reality is .
Check it with the town. Each outcome adds its chance times the log of a ratio:
- sun: 0.5 × ln(0.5 ÷ 0.9) ≈ 0.5 × (−0.588) ≈ −0.294
- rain: 0.5 × ln(0.5 ÷ 0.1) ≈ 0.5 × 1.609 ≈ 0.805
Together that's about 0.511 nats, the same as 1.204 − 0.693.
import math def kl(p, q): return sum(p_i * math.log(p_i / q_i) for p_i, q_i in zip(p, q) if p_i > 0) town = [0.5, 0.5] forecast_b = [0.9, 0.1] print(round(kl(town, forecast_b), 3), round(kl(forecast_b, town), 3))
It prints 0.511 0.368. The if p_i > 0 part skips outcomes that never happen, since they add nothing. The second number swaps the two lists, and the answer changes. Two facts are worth knowing:
- KL divergence is 0 exactly when , and positive otherwise.
- It isn't symmetric. Swapping and usually gives a different number. So it isn't a true distance. It's still the standard way to measure how different two sets of chances are.
You'll meet it in two places:
- RLHF and preference tuning, where people's ratings tune a model. KL is a penalty that keeps the tuned model's outputs close to the original model's.
- Distillation, where a small "student" model learns from a large "teacher". KL is the loss that makes the student's predictions match the teacher's.
import numpy as np def kl(p, q): p, q = np.asarray(p, float), np.asarray(q, float) mask = p > 0 return np.sum(p[mask] * np.log(p[mask] / q[mask])) teacher = [0.70, 0.20, 0.08, 0.02] student_close = [0.65, 0.22, 0.10, 0.03] student_far = [0.25, 0.25, 0.25, 0.25] print("KL(teacher || close student):", round(kl(teacher, student_close), 4)) print("KL(teacher || uniform): ", round(kl(teacher, student_far), 4)) print("KL(teacher || teacher): ", kl(teacher, teacher))
The close student scores 0.0069, almost nothing. A student that gives every word 0.25 scores 0.5344. The teacher compared with itself scores exactly 0.
Show the answer
0
KL is the extra cost of using the wrong forecast. When the two match, there is no extra cost, so it's exactly 0.
Perplexity: the loss as a number of choices
A loss like 1.5 nats is hard to picture. Perplexity turns it into something you can picture: a number of equally likely choices.
Take a fair six-sided die. Each face has a chance of 1/6. A model that always gives the right answer 1/6 has a loss of −ln(1/6) ≈ 1.792 nats per guess. Undo the log with exp: raise , a fixed number about 2.718, to that power. You get 6 back. So perplexity 6 means the model is, on average, as unsure as someone guessing a roll of a fair die.
For a whole text, average the loss over every token first:
Here is the number of tokens in the text. counts through them, from 1 to . is the chance the model gave to the token that really came at position .
In plain words: find the loss for every token and take the average. Then undo the log with exp. The result is how many equally likely choices the model is as unsure as.
Lower is better. A perfect model, always sure and always right, has perplexity 1. Phase 5 uses perplexity to compare language models.
Watch out: a loss in bits and a loss in nats aren't comparable. From nats you use exp, as here. From bits you raise 2 to the power instead.
Worked example: perplexity of a short text
A model reads three tokens. It gave the correct ones chances of 0.5, 0.2 and 0.1. Find each token's loss, the average loss, and the perplexity.
import math probs = [0.5, 0.2, 0.1] # chance given to each correct token losses = [-math.log(p) for p in probs] avg = sum(losses) / len(losses) print([round(x, 3) for x in losses]) print(round(avg, 3), round(math.exp(avg), 2))
The three losses are 0.693, 1.609 and 2.303 nats. Their average is 1.535 nats. Undo the log: exp(1.535) ≈ 4.64. On this text, the model is about as unsure as someone picking from 4 or 5 equally likely tokens.
Show the answer
4
Each token's loss is −ln 0.25 ≈ 1.386 nats, so the average is 1.386 too. exp(1.386) ≈ 4: as unsure as picking among 4 equally likely tokens.
Practice what you just read
- 6 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
With a one-hot target, what does cross-entropy loss reduce to?