Index Ai Academy
Phase 2 · Math, when you need it · Information

Cross-entropy and KL divergence

The loss function language models are trained on.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. Grading a weather forecast
  2. With one right answer, it's −log q
  3. How the loss steers training
  4. KL divergence: the extra cost
  5. Perplexity: the loss as a number of choices
  6. 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 qq, your surprise is −log⁡q-\log q. 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, pp, says how often each outcome really happens. For the town, pp is [0.5, 0.5].

The second, qq, is what you predicted. For forecaster B, qq is [0.9, 0.1].

H(p,q)=−∑ipilog⁡qiH(p, q) = -\sum_i p_i \log q_i

Here ii counts through the outcomes: sun, then rain. The ∑\sum sign (sigma) means "add up over every outcome". pip_i and qiq_i are the real and predicted chances of outcome ii.

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 ∑\sum 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 runs right here in your browser, with nothing to install.

It prints 1.204, as you worked out by hand.

Cross-entropy is smallest when the forecast matches reality, q=pq = p. Then it equals the entropy of pp itself. Any other forecast costs more. If qq gives a low chance to something that happens often, cross-entropy gets large.

Quick check
In the same town (rain on half the days), forecaster C says 70% rain every day. How does C's cross-entropy compare with A's 0.693 nats?
  1. Higher: only a 50/50 forecast reaches the minimum
  2. You can't compare two forecasts this way
  3. Lower: C is more confident, so C scores better
  4. The same: both forecasts add up to 1
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 qq 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 pp 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 pi=0p_i = 0 drops out. Only the correct word is left:

H(one-hot,q)=−log⁡qcorrectH(\text{one-hot}, q) = -\log q_{\text{correct}}

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 qcorrectq_{\text{correct}} gets close to 0, the loss shoots up toward infinity. Models are punished hard for being sure of the wrong thing.

Quick check
What does this print?
import math
prob_of_right = 1.0
print(-math.log(prob_of_right) + 0.0)
  1. 0.0
  2. inf
  3. -1.0
  4. 1.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.

Try it now

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.

Quick check
A model gives the correct next token probability 0.01. What's its loss for that token (natural log)?
  1. About 0.99
  2. About 2.0
  3. About 0.01
  4. About 4.6
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 ziz_i for the score of word ii. Write LL for the loss. When softmax feeds cross-entropy, the slope has a famously simple form:

∂L∂zi=qi−pi\frac{\partial L}{\partial z_i} = q_i - p_i

The curly d, ∂\partial, 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 cc. Softmax gives it the chance qc=ezc/∑jezjq_c = e^{z_c} / \sum_j e^{z_j}, where jj runs over every word. So the loss is:

L=−log⁡qc=−zc+log⁡∑jezjL = -\log q_c = -z_c + \log \sum_j e^{z_j}

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, ziz_i. The first part gives −1 when ii is the correct word, and 0 otherwise. That's −pi-p_i. The second part gives ezi/∑jezje^{z_i} / \sum_j e^{z_j}, which is softmax again: qiq_i. Together they make qi−piq_i - p_i.

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.

DKL(p ∥ q)=H(p,q)−H(p)=∑ipilog⁡piqiD_{\mathrm{KL}}(p \,\|\, q) = H(p, q) - H(p) = \sum_i p_i \log \frac{p_i}{q_i}

Here H(p)H(p) is the entropy of pp: the surprise of the best forecast. The double bar ∥\| only separates the two lists. The first list, pp, is reality. The second, qq, 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 qq when reality is pp.

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 q=pq = p, and positive otherwise.
  • It isn't symmetric. Swapping pp and qq 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.

Quick check
A student model's predictions match the teacher's exactly, word for word. What is the KL divergence of the student from the teacher?
  1. The teacher's entropy
  2. It can't be computed
  3. 0
  4. 1
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 ee, 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:

PPL=exp⁡(1N∑t=1N−log⁡q(tokent))\text{PPL} = \exp\Big(\frac{1}{N}\sum_{t=1}^{N} -\log q(\text{token}_t)\Big)

Here NN is the number of tokens in the text. tt counts through them, from 1 to NN. q(tokent)q(\text{token}_t) is the chance the model gave to the token that really came at position tt.

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

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))
In the lesson you can change this and run it.

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.

Quick check
A model gives every correct token a chance of 0.25. What is its perplexity?
  1. 4
  2. 2
  3. 0.25
  4. 1.386
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

Create a free account

Try a quiz question

With a one-hot target, what does cross-entropy loss reduce to?

  1. −log of the probability given to the correct answer
  2. The sum of all the probabilities, which is 1
  3. The entropy of the prediction, on its own
  4. The squared error between prediction and target

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson