KL Divergence, Cross-Entropy
& Machine Learning

How many extra bits do you pay for believing the wrong thing?
That one number trains almost every modern model.

Cross-entropy

Average code length when you code with the wrong model.

KL divergence

The extra bits. Never negative. Zero only when you are right.

Learning

Max likelihood = min KL. Softmax loss = cross-entropy.

Modern ML

VAEs, GANs, distillation, RLHF: all KL in disguise.

Definitions, theorems with proof sketches, and runnable stdlib-only Python on every key idea. Outputs shown are from real runs (Python 3.11+).

Roadmap WHERE WE GO

  1. Surprise & entropy recap
  2. Cross-entropy: coding with the wrong model
  3. KL divergence: definition & properties
  4. Gibbs' inequality (proof)
  5. H(p,q) = H(p) + KL(p‖q)
  6. Asymmetry, worked example
  7. Forward vs reverse KL
  8. Support & infinite KL
  9. Maximum likelihood = minimum KL
  10. Softmax + cross-entropy gradient
  11. Logistic regression from scratch
  12. Why not squared error?
  13. Label smoothing
  14. Perplexity & bits per token
  15. Mutual information as a KL
  16. f-divergences & Jensen–Shannon
  17. GANs
  18. Variational inference & the ELBO
  19. VAEs & Gaussian KL
  20. RLHF KL penalty
  21. Knowledge distillation
  22. Numerical stability
  23. History, pitfalls, summary

Surprise & Entropy RECAP

Surprise (self-information) of an outcome with probability p(x):

I(x) = −log2 p(x)   bits

Rare things surprise us a lot. Sure things do not surprise us at all: −log 1 = 0.

Entropy is the average surprise:

H(p) = −Σx p(x) log2 p(x)

Shannon's source coding theorem (1948). No lossless code can use fewer than H(p) bits per symbol on average. Good codes (Huffman, arithmetic) get within 1 bit, or arbitrarily close for long blocks.

A worked source

xp(x)−log2pideal code
A1/210
B1/4210
C1/83110
D1/83111

H = ½·1 + ¼·2 + ⅛·3 + ⅛·3 = 1.75 bits

Key idea: an ideal code gives symbol x a codeword of length −log p(x). Your model of the world is your code.

Units: log base 2 gives bits. Natural log gives nats. 1 nat = 1.4427 bits. ML libraries use nats.

Cross-Entropy: Coding With the Wrong Model DEFINITION

Cross-entropy of model q on data from p:

H(p, q) = −Σx p(x) log q(x) = Ex~p[−log q(x)]

You build the code from q (lengths −log q(x)). But the symbols really arrive from p. So this is the average length you actually pay.

Back to our source

Suppose we think all four letters are equally likely. Then q = (¼, ¼, ¼, ¼) and every codeword has 2 bits.

H(p,q) = 2 bits   vs   H(p) = 1.75 bits

We waste 0.25 bits per symbol. That waste has a name.

from math import log2

def entropy(p):
    return -sum(pi * log2(pi) for pi in p if pi > 0)

def cross_entropy(p, q):
    return -sum(pi * log2(qi) for pi, qi in zip(p, q) if pi > 0)

def kl(p, q):
    return sum(pi * log2(pi / qi) for pi, qi in zip(p, q) if pi > 0)

p = [0.5, 0.25, 0.125, 0.125]   # true source
q = [0.25, 0.25, 0.25, 0.25]    # model: uniform
print(f"H(p)   = {entropy(p):.3f} bits")
print(f"H(p,q) = {cross_entropy(p, q):.3f} bits")
print(f"KL     = {kl(p, q):.3f} bits")
assert abs(cross_entropy(p, q) - (entropy(p) + kl(p, q))) < 1e-12
H(p)   = 1.750 bits
H(p,q) = 2.000 bits
KL     = 0.250 bits

The if pi > 0 guard uses the convention 0 log 0 = 0. It is the limit of t log t as t → 0.

Kullback–Leibler Divergence DEFINITION

KL divergence (relative entropy) from q to p:

KL(p ‖ q) = Σx p(x) log p(x)⁄q(x) = Ex~p[ log p(x) − log q(x) ]

Read it as: the expected extra bits per symbol when you code data from p with a code built for q.

Continuous version with densities:

KL(p ‖ q) = ∫ p(x) log p(x)⁄q(x) dx

Unlike differential entropy, KL keeps its meaning for densities. It stays ≥ 0 and does not change if you rescale x.

Properties

PropertyHolds?
KL(p‖q) ≥ 0yes (Gibbs)
KL(p‖q) = 0 ⇔ p = qyes
Symmetricno
Triangle inequalityno
Convex in the pair (p, q)yes
Additive for independent pairsyes
Invariant under invertible maps of xyes
Can be infiniteyes

It is a divergence, not a distance. It fails symmetry and the triangle inequality. Its square root is not a metric either.

Gibbs' Inequality THEOREM + PROOF

Theorem (Gibbs). For any distributions p, q on the same set, KL(p ‖ q) ≥ 0. Equality holds iff p = q.

Proof with ln t ≤ t − 1

Let S = {x : p(x) > 0}. Work in nats.

−KL(p‖q) = Σx∈S p(x) ln q(x)⁄p(x)

  ≤ Σx∈S p(x) ( q(x)⁄p(x) − 1 )

  = Σx∈S q(x) − 1  ≤  1 − 1 = 0

The first step is tight only when q(x)/p(x) = 1 on all of S. The last step is tight only when q puts no mass outside S. Together they give p = q. ∎

Second proof: Jensen

log is concave. So E[log Y] ≤ log E[Y]. Take Y = q(X)/p(X) with X ~ p:

−KL = E[log Y] ≤ log E[Y] = log ΣS q(x) ≤ log 1 = 0

Sanity check: try to beat it

import random
from math import log2
def kl(p, q):
    return sum(pi * log2(pi / qi) for pi, qi in zip(p, q) if pi > 0)

def rand_dist(k):
    w = [random.random() for _ in range(k)]
    s = sum(w)
    return [x / s for x in w]

random.seed(0)
worst = min(kl(rand_dist(5), rand_dist(5)) for _ in range(100_000))
print(f"smallest KL over 100k random pairs: {worst:.5f}")
smallest KL over 100k random pairs: 0.00076

Corollary: H(p) ≤ log |X|. Take q uniform: KL(p‖u) = log|X| − H(p) ≥ 0.

Cross-Entropy = Entropy + KL IDENTITY

Identity. H(p, q) = H(p) + KL(p ‖ q)

One-line proof

H(p,q) = −Σ p log q

  = −Σ p log p + Σ p log (p/q)

  = H(p) + KL(p‖q)   ∎

Why ML cares

H(p) is a property of the data. You cannot change it. So when you train q:

argminq H(p,q) = argminq KL(p‖q)

Minimizing cross-entropy loss is minimizing KL. The loss floor is H(p), not 0.

Bits per symbol for our 4-letter source true code p H(p) = 1.75 uniform code q H(p) = 1.75 .25 KL(p‖q) H(p,q) = 2.00 bits actually paid 0 1 2

The assert in the cross-entropy snippet checks this identity numerically. Try it yourself on a few random pairs.

Worked Example: H, CE and KL by Hand STEP BY STEP

True weather p and a forecaster's model q over sun, cloud, rain. All logs base 2.

xpqp·(−log p)p·(−log q)p·log(p/q)
sun1/21/40.51.0+0.5
cloud1/41/40.50.50
rain1/41/20.50.25−0.25
column sumsH(p) = 1.5H(p,q) = 1.75KL = 0.25
  1. Step 1. Code lengths: −log2 q = 2, 2, 1 bits under the model.
  2. Step 2. Weight each length by the true p: that is H(p,q).
  3. Step 3. Check the identity: 1.75 = 1.5 + 0.25. It holds.

Read the answer

The forecaster pays 1.75 bits per day. A perfect model would pay 1.5. The 0.25 bits per day is the price of believing q when the truth is p.

Common mistake: negative terms

The rain row of KL is −0.25. Single terms can be negative, where q > p. Only the sum is ≥ 0 (Gibbs). Do not "fix" a negative term with abs.

Common mistake: units

In nats the same numbers are 1.040, 1.213 and 0.173. Multiply bits by ln 2 ≈ 0.693 to get nats. PyTorch and NumPy use nats.

Here KL(q‖p) is also 0.25, because q is p with two labels swapped. That is luck. The next slide shows the usual case.

KL Is Not Symmetric WORKED EXAMPLE

By hand: biased coin p = (0.9, 0.1), fair coin q = (0.5, 0.5)

KL(p‖q) = 0.9 log2(0.9/0.5) + 0.1 log2(0.1/0.5)

  = 0.9(0.848) + 0.1(−2.322) = 0.531

KL(q‖p) = 0.5 log2(0.5/0.9) + 0.5 log2(0.5/0.1)

  = 0.5(−0.848) + 0.5(2.322) = 0.737

from math import log2
def kl(p, q):
    return sum(pi * log2(pi / qi) for pi, qi in zip(p, q) if pi > 0)

p = [0.9, 0.1]
q = [0.5, 0.5]
print(f"KL(p||q) = {kl(p, q):.4f}")
print(f"KL(q||p) = {kl(q, p):.4f}")
print(f"KL(p||p) = {kl(p, p):.4f}")
KL(p||q) = 0.5310
KL(q||p) = 0.7370
KL(p||p) = 0.0000

Why the difference?

KL(q‖p) weights by q. Under the fair coin, tails comes up half the time. But p gave tails only 0.1. So the code built from p spends 3.32 bits on each tail. That hurts a lot.

KL(p‖q) weights by p. Tails is rare there, so the fair code's small waste on heads dominates.

Rule of thumb: KL punishes the model hard where the first argument has mass but the second does not.

No triangle inequality either

Take a=(.9,.1), b=(.5,.5), c=(.1,.9). Then KL(a‖c) = 2.536 bits. But KL(a‖b) + KL(b‖c) = 0.531 + 0.737 = 1.268.

Forward vs Reverse KL MODE COVERING vs MODE SEEKING

Forward: min q KL(p ‖ q) Reverse: min q KL(q ‖ p) q = N(0, 2.09²): spreads over both humps q = N(2, 0.6²): locks onto one hump p (bimodal target) best single Gaussian q

Forward KL(p‖q) — "inclusive"

Expectation is under p. If q ≈ 0 where p > 0, the cost blows up. So q must cover every mode, even if it puts mass in the gap. For a Gaussian q the optimum just matches the mean and variance of p. Used by MLE and supervised learning.

Reverse KL(q‖p) — "exclusive"

Expectation is under q. Mass of q where p ≈ 0 is very costly. Missing a mode costs nothing. So q picks one mode and fits it tightly. Used by variational inference and RLHF. It gives sharp but overconfident fits.

Support & Infinite KL EDGE CASES

Absolute continuity. p ≪ q means: whenever q(x) = 0, also p(x) = 0.

KL(p‖q) < ∞  requires  p ≪ q

If p(x) > 0 = q(x) for some x, the term p(x) log(p(x)/0) = +∞. Your code has no codeword for a symbol that really occurs.

p(x)q(x)term p log(p/q)
0anything0 (by convention)
> 0> 0finite
> 00+∞

What this means in practice

  • A language model that gives a real next token probability 0 has infinite loss. That is why softmax never outputs exact zeros.
  • Count-based models need smoothing (add-one, Kneser–Ney) for unseen events.
  • Two distributions with disjoint support have KL = ∞ both ways. KL then gives no gradient signal at all. This motivates JS and Wasserstein distances.
  • In code, log(0) raises ValueError in Python and gives -inf in NumPy. Clamp with a tiny ε or work in log-space.

Forward KL is "zero-avoiding": q must be > 0 wherever p is. Reverse KL is "zero-forcing": q must be 0 wherever p is.

Maximum Likelihood = Minimum KL THEOREM

Data x1, …, xN. The empirical distribution puts mass 1/N on each sample:

p̂(x) = #{i : xi = x}⁄N

Theorem. For any model family qθ, argmaxθ ∏i qθ(xi) = argminθ KL(p̂ ‖ qθ).

Proof

−1⁄N log ∏i qθ(xi) = −1⁄N Σi log qθ(xi)

  = −Σx p̂(x) log qθ(x)    (group equal x)

  = H(p̂, qθ) = H(p̂) + KL(p̂ ‖ qθ)

H(p̂) does not depend on θ, and −log is decreasing. ∎

Check it: 10 coin flips, 7 heads. Scan θ on a grid.

from math import log
flips = "HHTHHHTHHT"                  # data: 7 heads, 3 tails
p_hat = flips.count("H") / len(flips) # empirical distribution

def nll(theta):                       # average negative log-likelihood
    return -sum(log(theta if c == "H" else 1 - theta) for c in flips) / len(flips)

def kl_emp(theta):                    # KL(p_hat || Bernoulli(theta))
    return (p_hat * log(p_hat / theta)
            + (1 - p_hat) * log((1 - p_hat) / (1 - theta)))

grid = [i / 100 for i in range(1, 100)]
print("argmin NLL :", min(grid, key=nll))
print("argmin KL  :", min(grid, key=kl_emp))
h = -(p_hat * log(p_hat) + (1 - p_hat) * log(1 - p_hat))
print(all(abs(nll(t) - (h + kl_emp(t))) < 1e-12 for t in grid))
argmin NLL : 0.7
argmin KL  : 0.7
True

The last line confirms NLL(θ) = H(p̂) + KL(p̂‖qθ) for all 99 grid values. The closed form is θ̂ = 7/10.

As N → ∞, p̂ → p. So MLE is a sample estimate of "minimize KL from the truth". This is the forward, mode-covering direction.

Softmax + Cross-Entropy DERIVATION

A classifier outputs logits z ∈ ℝK. Softmax turns them into a distribution:

qi = ezi / Σj ezj

The target is a one-hot y (true class c). Cross-entropy loss:

L = H(y, q) = −log qc = −zc + log Σj ezj

Gradient

∂L/∂zi = −[i = c] + ezi/Σj ezj

∇z L = q − y

"Prediction minus target." It is bounded in [−1, 1], so it never explodes. It also never vanishes while you are wrong.

The gradient entries sum to 0 because softmax only cares about differences of logits. Adding a constant to every zi changes nothing.

Finite-difference check (central differences, h = 10−6):

from math import exp, log

def softmax(z):
    m = max(z)
    e = [exp(v - m) for v in z]
    s = sum(e)
    return [v / s for v in e]

def ce_loss(z, y):              # y = index of the true class
    return -log(softmax(z)[y])

def ce_grad(z, y):              # analytic: p - onehot(y)
    p = softmax(z)
    return [pi - (1 if i == y else 0) for i, pi in enumerate(p)]

z, y, h = [2.0, -1.0, 0.5], 0, 1e-6
numeric = []
for i in range(len(z)):
    zp = z[:]; zp[i] += h
    zm = z[:]; zm[i] -= h
    numeric.append((ce_loss(zp, y) - ce_loss(zm, y)) / (2 * h))
print("analytic:", [round(g, 6) for g in ce_grad(z, y)])
print("numeric :", [round(g, 6) for g in numeric])
analytic: [-0.214403, 0.039113, 0.17529]
numeric : [-0.214403, 0.039113, 0.17529]

Logistic Regression From Scratch BINARY CROSS-ENTROPY

import random
from math import exp, log

random.seed(1)
# 2-D points: class 1 if x + y > 1 (with a little label noise)
data = []
for _ in range(200):
    x, y = random.random(), random.random()
    label = 1 if x + y > 1 else 0
    if random.random() < 0.05:
        label = 1 - label
    data.append(((x, y), label))

def sigmoid(t):
    return 1 / (1 + exp(-t))

w1 = w2 = b = 0.0
lr = 1.0
for epoch in range(2001):
    g1 = g2 = gb = loss = 0.0
    for (x, y), t in data:
        p = sigmoid(w1 * x + w2 * y + b)
        loss -= t * log(p) + (1 - t) * log(1 - p)   # binary cross-entropy
        g1 += (p - t) * x; g2 += (p - t) * y; gb += p - t
    n = len(data)
    w1 -= lr * g1 / n; w2 -= lr * g2 / n; b -= lr * gb / n
    if epoch % 500 == 0:
        print(f"epoch {epoch:4d}  BCE = {loss / n:.4f}")

acc = sum((sigmoid(w1*x + w2*y + b) > 0.5) == t for (x, y), t in data) / len(data)
print(f"accuracy = {acc:.2f}")
epoch    0  BCE = 0.6931
epoch  500  BCE = 0.3724
epoch 1000  BCE = 0.3668
epoch 1500  BCE = 0.3661
epoch 2000  BCE = 0.3660
accuracy = 0.88

Binary cross-entropy is softmax cross-entropy with K = 2:

L = −[t log p + (1−t) log(1−p)],   p = σ(w·x + b)

∇w L = (p − t) x

  • Start: all weights 0, so p = 0.5 and loss = ln 2 = 0.6931 nats.
  • The loss is convex in (w, b), so gradient descent finds the global min.
  • The floor is not 0. We flipped 5% of labels on purpose, so some cost is H(p) of the noise.
  • Accuracy is 0.88. Those 5% noise flips plus boundary points cap it.

Why Not Squared Error? GRADIENTS

Compare gradients w.r.t. the logit z

Binary case, p = σ(z), true label t = 1:

CE:   ∂L/∂z = p − 1

MSE: ∂L/∂z = 2(p − 1) · p(1 − p)

The MSE gradient has an extra σ'(z) = p(1−p) factor. When the model is confidently wrong (p → 0), that factor goes to 0. Learning stalls exactly when it matters most.

p (for true class)|CE grad||MSE grad|
0.50.500.25
0.10.900.162
0.010.990.0196
0.00010.99990.0002

The deeper reasons

  • Probabilistic: CE is the negative log-likelihood of a categorical model. MSE is the NLL of a Gaussian. Labels are not Gaussian.
  • Proper scoring rule: expected CE is minimized only by reporting your true belief. (So is the Brier score, which is MSE on probabilities. But its gradient in logit space still saturates.)
  • Convexity: CE with softmax is convex in the logits. MSE through a sigmoid is not.
  • Information: CE is measured in bits. It says how many bits your model would need to encode the labels.

MSE is still right for regression with Gaussian noise. That is MLE too, just with a different model.

Label Smoothing REGULARIZATION

Replace the one-hot target with a softened one (Szegedy et al., 2016):

yLS = (1 − ε) y + ε / K

The loss then splits:

H(yLS, q) = (1−ε) H(y, q) + ε H(u, q)

where u is uniform. The second term is a KL-to-uniform penalty (up to a constant). It pulls q away from extreme confidence.

Effects

  • Better calibration: probabilities match accuracy more closely.
  • Logits stay bounded. Without smoothing, the optimum pushes zc → ∞.
  • Downside: it hurts distillation. The teacher's "dark knowledge" gets washed out.
from math import log
def ce(target, pred):
    return -sum(t * log(p) for t, p in zip(target, pred) if t > 0)

def smooth(k, y, eps):
    return [(1 - eps) + eps / k if i == y else eps / k for i in range(k)]

confident = [0.998, 0.001, 0.001]
calibrated = [0.90, 0.05, 0.05]
for eps in (0.0, 0.1):
    t = smooth(3, 0, eps)
    print(f"eps={eps}: loss(confident)={ce(t, confident):.3f}"
          f"  loss(calibrated)={ce(t, calibrated):.3f}")
eps=0.0: loss(confident)=0.002  loss(calibrated)=0.105
eps=0.1: loss(confident)=0.462  loss(calibrated)=0.298

With hard labels, the 99.8%-confident model wins. With ε = 0.1, the calmer 90% model wins. The optimum is now q = yLS, not a spike.

Perplexity & Bits per Token LANGUAGE MODELS

A language model predicts q(wt | w<t). On a test text of n tokens:

CE = −1⁄n Σt log2 q(wt | w<t)   bits/token

Perplexity = 2CE

Reading perplexity

  • Perplexity k means the model is as unsure as if it picked uniformly among k words.
  • Uniform over vocab V gives perplexity V.
  • Compare only with the same tokenizer. Bits per byte or per character is fairer across tokenizers.
  • A model with CE c bits/token plus an arithmetic coder compresses text to about c bits/token. Better LM = better compressor.
from collections import Counter
from math import log2

train = "the cat sat on the mat . the dog sat on the log . the cat saw the dog .".split()
test = "the dog sat on the mat .".split()
vocab = set(train)
V = len(vocab)
bigrams = Counter(zip(train, train[1:]))
unigrams = Counter(train[:-1])

def prob(prev, word):                 # add-one (Laplace) smoothing
    return (bigrams[(prev, word)] + 1) / (unigrams[prev] + V)

bits = -sum(log2(prob(a, b)) for a, b in zip(test, test[1:]))
n = len(test) - 1
print(f"vocab={V}  cross-entropy={bits / n:.3f} bits/token")
print(f"perplexity={2 ** (bits / n):.2f}  (uniform guess would be {V})")
vocab=9  cross-entropy=2.293 bits/token
perplexity=4.90  (uniform guess would be 9)

Add-one smoothing keeps every bigram probability above 0. Without it, any unseen pair in a test sentence, such as ("cat", "log"), would get probability 0 and infinite perplexity.

Mutual Information Is a KL DEPENDENCE

Mutual information between X and Y:

I(X; Y) = KL( p(x, y) ‖ p(x) p(y) )

It is how far the joint is from "independent". So by Gibbs, I ≥ 0, with equality iff X ⊥ Y.

Equivalent forms

I(X;Y) = H(X) − H(X|Y)

  = H(X) + H(Y) − H(X,Y)

  = Ey[ KL( p(x|y) ‖ p(x) ) ]

The last form says: MI is the average information gain about X from seeing Y.

Seeing the umbrella removes about half of our 0.93 bits of doubt about the weather.

from math import log2
# joint distribution of (weather, umbrella)
joint = {("rain", "yes"): 0.30, ("rain", "no"): 0.05,
         ("sun",  "yes"): 0.05, ("sun",  "no"): 0.60}
px, py = {}, {}
for (x, y), p in joint.items():
    px[x] = px.get(x, 0) + p
    py[y] = py.get(y, 0) + p

mi = sum(p * log2(p / (px[x] * py[y])) for (x, y), p in joint.items())
hx = -sum(p * log2(p) for p in px.values())
print(f"I(X;Y) = {mi:.4f} bits   H(X) = {hx:.4f} bits")
I(X;Y) = 0.4727 bits   H(X) = 0.9341 bits

In ML

  • Decision trees split on the feature with the largest information gain = MI.
  • InfoNCE / contrastive learning (CLIP, SimCLR) maximizes a lower bound on MI.
  • Feature selection, the information bottleneck, and channel capacity all use MI.

f-Divergences & Jensen–Shannon GENERALIZATION

f-divergence (Csiszár 1963; Ali & Silvey 1966). For convex f with f(1) = 0:

Df(p ‖ q) = Σx q(x) f( p(x) / q(x) )

Jensen gives Df ≥ f(1) = 0, the same proof as Gibbs.

f(t)divergence
t log tKL(p‖q)
−log tKL(q‖p)
(t − 1)²Pearson χ²
½|t − 1|total variation
(√t − 1)²squared Hellinger (×2)
t log t − (t+1) logt+1⁄22 · Jensen–Shannon

Pinsker: TV(p,q) ≤ √( KL(p‖q) / 2 ) in nats. Small KL forces the distributions to be close.

Jensen–Shannon divergence. With m = ½(p + q):

JS(p, q) = ½ KL(p‖m) + ½ KL(q‖m)

Symmetric. Always finite, between 0 and 1 bit. Its square root is a metric.

from math import log2
def kl(p, q):
    return sum(pi * log2(pi / qi) for pi, qi in zip(p, q) if pi > 0)

def js(p, q):
    m = [(a + b) / 2 for a, b in zip(p, q)]
    return 0.5 * kl(p, m) + 0.5 * kl(q, m)

p = [1.0, 0.0]
q = [0.0, 1.0]
print(f"JS(p,q) = {js(p, q):.4f} bits   JS(q,p) = {js(q, p):.4f}")
try:
    kl(p, q)
except ZeroDivisionError:
    print("KL(p||q) = infinity (q is 0 where p is not)")
JS(p,q) = 1.0000 bits   JS(q,p) = 1.0000
KL(p||q) = infinity (q is 0 where p is not)

GANs Minimize Jensen–Shannon GOODFELLOW 2014

Generator G makes fakes with law pg. Discriminator D(x) guesses "real?". The game:

minG maxD Epdata[log D(x)] + Epg[log(1 − D(x))]

This is binary cross-entropy for D, with the sign flipped.

Proposition. For fixed G, the best discriminator is D*(x) = pdata(x) / (pdata(x) + pg(x)). Plugging it in gives V(G, D*) = −log 4 + 2·JS(pdata, pg) in nats.

Proof sketch

Pointwise, a log D + b log(1−D) peaks at D = a/(a+b). Substitute and write each term as a KL to the mixture m = (pdata + pg)/2. The constants add up to −log 4. ∎

Why GANs are hard to train

  • Real images sit on a thin manifold. Early on, pg and pdata barely overlap.
  • Disjoint supports make JS = log 2, a constant. The generator gets no gradient.
  • We saw this with JS = 1.0000 bits on the last slide.

Fixes

  • Non-saturating loss: maximize log D(G(z)) instead.
  • f-GAN (Nowozin 2016): train with any f-divergence.
  • WGAN (Arjovsky 2017): Wasserstein distance. It stays smooth for disjoint supports.

Variational Inference & the ELBO DERIVATION

Latent model p(x, z) = p(z) p(x|z). We want the posterior p(z|x). But p(x) = ∫ p(x,z) dz is intractable.

Pick a simple family qφ(z) and minimize the reverse KL KL(q ‖ p(z|x)).

The key identity

log p(x) = Eq[ log p(x,z) − log q(z) ] + KL( q(z) ‖ p(z|x) )

          = ELBO(q) + KL(q ‖ posterior)

Proof: write log p(x) = log p(x,z) − log p(z|x) for any z. Add and subtract log q(z). Take Eq. ∎

ELBO KL gap log p(x): fixed raise ELBO ⇒ the gap shrinks (log p(x) does not depend on q) ELBO ≤ log p(x) always, since KL ≥ 0

Two readings of the ELBO

ELBO = Eq[log p(x|z)] − KL( q(z) ‖ p(z) )

Term 1: reconstruct the data well. Term 2: keep the code close to the prior. That is the VAE loss.

VAEs & the Gaussian KL CLOSED FORM

Gaussian KL. For univariate Gaussians, KL( N(μ1, σ1²) ‖ N(μ2, σ2²) ) = log(σ2/σ1) + (σ1² + (μ1 − μ2)²) / (2σ2²) − ½

With the VAE prior N(0, 1) it becomes

KL = ½( σ² + μ² − 1 ) − log σ

For a d-dim diagonal Gaussian, sum this over the d coordinates (additivity).

The VAE recipe (Kingma & Welling 2013)

  1. Encoder maps x to (μ, log σ²).
  2. Reparameterize: z = μ + σ · ε with ε ~ N(0,1). Now gradients flow through the sample.
  3. Decoder gives p(x|z). Loss = reconstruction NLL + closed-form KL.
  4. β-VAE scales the KL term by β to trade quality for cleaner latents.
import random
from math import log, pi, exp, sqrt

def kl_gauss(mu, sigma):          # KL( N(mu, sigma^2) || N(0, 1) )
    return 0.5 * (sigma**2 + mu**2 - 1) - log(sigma)

def logpdf(x, mu, s):
    return -0.5 * log(2 * pi * s * s) - (x - mu) ** 2 / (2 * s * s)

random.seed(0)
mu, sigma = 1.0, 0.5
xs = [random.gauss(mu, sigma) for _ in range(200_000)]
mc = sum(logpdf(x, mu, sigma) - logpdf(x, 0, 1) for x in xs) / len(xs)
print(f"closed form : {kl_gauss(mu, sigma):.4f} nats")
print(f"Monte Carlo : {mc:.4f} nats")
closed form : 0.8181 nats
Monte Carlo : 0.8206 nats

Monte Carlo averages log q(z) − log p(z) over samples from q. It agrees to 2 decimals with 200k samples. The closed form has no noise, which is why VAEs use it.

The KL Penalty in RLHF ALIGNMENT

Fine-tune a language model π against a learned reward r(x, y). Keep it near the reference model πref:

maxπ Ey~π[ r(x, y) ] − β · KL( π(·|x) ‖ πref(·|x) )

Closed-form optimum. π*(y|x) ∝ πref(y|x) · exp( r(x,y) / β ). This follows from Gibbs: the objective equals −β KL(π ‖ π*) + const.

Why the penalty?

  • The reward model is imperfect. Unchecked, the policy finds its blind spots (reward hacking).
  • It keeps fluency and knowledge from pre-training.
  • DPO (Rafailov 2023) inverts the formula above. It trains on preferences directly, without a reward model.
from math import exp, log

ref = {"helpful": 0.40, "verbose": 0.30, "flattery": 0.20, "rude": 0.10}
reward = {"helpful": 1.0, "verbose": 0.6, "flattery": 1.5, "rude": -2.0}

def optimal_policy(beta):           # argmax E[r] - beta * KL(pi || ref)
    w = {y: ref[y] * exp(reward[y] / beta) for y in ref}
    z = sum(w.values())
    return {y: v / z for y, v in w.items()}

for beta in (10.0, 1.0, 0.1):
    pi = optimal_policy(beta)
    kl = sum(p * log(p / ref[y]) for y, p in pi.items())
    print(f"beta={beta:<4}", {y: round(p, 2) for y, p in pi.items()},
          f"KL={kl:.3f}")
beta=10.0 {'helpful': 0.41, 'verbose': 0.3, 'flattery': 0.22, 'rude': 0.08} KL=0.004
beta=1.0  {'helpful': 0.43, 'verbose': 0.21, 'flattery': 0.35, 'rude': 0.01} KL=0.141
beta=0.1  {'helpful': 0.01, 'verbose': 0.0, 'flattery': 0.99, 'rude': 0.0} KL=1.528

The reward model overrates "flattery". With large β the policy barely moves. With β = 0.1 it puts 99% on flattery. That is reward hacking in four lines.

This is reverse KL (π first), so it is mode-seeking. PPO usually adds it as a per-token penalty.

Knowledge Distillation HINTON 2015

Train a small student to match a big teacher's softened outputs. With temperature T:

piT = softmax(z / T)i

L = (1−α) H(y, q) + α T² KL( pTteacher ‖ qTstudent )

Why it works

  • "Dark knowledge": the teacher says a cat looks more like a dog than a car. One-hot labels hide that.
  • Higher T flattens the distribution. Small probabilities get a real vote.
  • Gradients of the soft term scale like 1/T². So we multiply by T² to keep both terms balanced.
  • Used for DistilBERT, small LLMs, and on-device models.
from math import exp, log
def softmax(z, T=1.0):
    m = max(z)
    e = [exp((v - m) / T) for v in z]
    s = sum(e)
    return [v / s for v in e]

teacher_logits = [6.0, 2.5, 2.0, -1.0]      # cat, dog, fox, car
for T in (1, 4):
    print(f"T={T}:", [round(p, 3) for p in softmax(teacher_logits, T)])

def distill_loss(student, teacher, T):
    p, q = softmax(teacher, T), softmax(student, T)
    return T * T * sum(pi * log(pi / qi) for pi, qi in zip(p, q))

print(f"KD loss = {distill_loss([4.0, 1.0, 1.5, 0.0], teacher_logits, 4):.4f}")
T=1: [0.953, 0.029, 0.017, 0.001]
T=4: [0.511, 0.213, 0.188, 0.089]
KD loss = 0.4710

At T = 1 the three wrong classes share under 5%, so they barely teach anything. At T = 4, dog and fox get about 20% each while car gets 9%. The student learns what looks like a cat.

Numerical Stability LOG-SUM-EXP

Log-sum-exp trick. For any constant m:

log Σj ezj = m + log Σj ezj − m

Pick m = max z. Then every exponent is ≤ 0. Nothing overflows, and the largest term is exactly 1.

from math import exp, log

def logsumexp(z):
    m = max(z)
    return m + log(sum(exp(v - m) for v in z))

z = [1000.0, 999.0, 998.0]
try:
    naive = log(sum(exp(v) for v in z))
except OverflowError:
    naive = "OverflowError"
print("naive :", naive)
print("stable:", round(logsumexp(z), 6))
# log-softmax, then cross-entropy for class 0, never forming tiny probs
print("CE    :", round(logsumexp(z) - z[0], 6))
naive : OverflowError
stable: 1000.407606
CE    : 0.407606

Common bugs

  • log(softmax(z)) in two steps. Tiny probabilities round to 0, then log gives -inf. Use a fused log_softmax or cross_entropy(logits, y).
  • Passing probabilities where the API wants logits. The loss then applies softmax twice.
  • Clamping with ε too large (like 1e-3). It caps the loss and biases the gradient.
  • KLDivLoss in PyTorch expects log-probs for the input and probs for the target. The argument order is (q, p), which confuses many.
  • Mixing bits and nats when you compare numbers from different tools.

Stay in log-space as long as you can. Exponentiate only at the very end, if ever.

Common Mistakes STUDENT TRAPS

Bits vs nats

np.log and PyTorch losses use nats. A loss of 0.693 nats is 1 bit. Perplexity is 2CE for bits but eCE for nats. Mixing them gives nonsense.

KL direction

KL(p‖q) averages over p, the first argument. Swapping changes the number and the behavior: forward covers all modes, reverse picks one. In PyTorch, kl_div(log_q, p) computes KL(p‖q).

Zeros

0 · log(0/q) = 0: safe to skip. But p · log(p/0) = ∞ for p > 0. A model that says "impossible" to something that happens pays infinite loss.

"KL is a distance"

It is not symmetric, and the triangle rule fails. For coins with heads-probability 0.1, 0.5 and 0.9 (in nats): KL(0.1‖0.9) = 1.758, but KL(0.1‖0.5) + KL(0.5‖0.9) = 0.368 + 0.511 = 0.879.

"Loss should reach 0"

Cross-entropy on soft or noisy labels bottoms out at H(p), not 0. With label smoothing ε = 0.1 and 3 classes, the floor is H(yLS) ≈ 0.291 nats.

Probabilities vs logits

cross_entropy(logits, y) applies softmax itself. Passing probabilities applies it twice. The loss still falls, but the gradients are wrong.

Check Yourself EXERCISES

  1. A classifier gives the true class probability 0.25. What is the cross-entropy loss in bits? In nats?
  2. True label p = (1, 0), model q = (0.9, 0.1). Find KL(p‖q) and KL(q‖p) in nats.
  3. A language model has cross-entropy 3 bits per token. What is its perplexity?
  4. Logits z = (2, 1, 0), true class 0. Find softmax, the loss in nats, and ∇z of the loss.
  5. Find KL( N(0, 22) ‖ N(0, 1) ) in nats.
  6. Your 3-class loss is stuck at 0.30 nats with ε = 0.1 smoothing. Is the model broken?

Try each one before you look to the right.

Answers

  1. −log2 0.25 = 2 bits. −ln 0.25 ≈ 1.386 nats.
  2. KL(p‖q) = −ln 0.9 ≈ 0.105. KL(q‖p) = ∞, because q puts 0.1 where p is 0.
  3. 23 = 8: as unsure as a uniform pick among 8 tokens.
  4. ez = (7.389, 2.718, 1), sum 11.107, so q ≈ (0.665, 0.245, 0.090). Loss = −ln 0.665 ≈ 0.408. Gradient q − y ≈ (−0.335, 0.245, 0.090).
  5. ln(1/2) + 4/2 − ½ ≈ −0.693 + 1.5 = 0.807.
  6. Not necessarily. The floor is H(p), not 0. With ε = 0.1, 3 classes, that floor is 0.291 nats, so 0.30 is close to it.

A Short History TIMELINE

YearWhoWhat
1902J. W. GibbsThe inequality, in statistical mechanics
1906J. JensenJensen's inequality for convex functions
1948C. ShannonEntropy, source coding, the bit
1951S. Kullback & R. Leibler"On Information and Sufficiency": the divergence we now call KL
1957E. T. JaynesMaximum entropy principle
1963–66I. Csiszár; Ali & Silveyf-divergences
1973H. AkaikeAIC: model selection as estimated KL
1991J. LinJensen–Shannon divergence
1999Jordan et al.Variational methods for graphical models
2013Kingma & WellingVariational autoencoders
2014Goodfellow et al.GANs and the JS connection
2015Hinton, Vinyals, DeanKnowledge distillation
2015–16Szegedy et al.Label smoothing (Inception v3)
2017–22Christiano; Ouyang et al.RLHF with a KL penalty (InstructGPT)
2023Rafailov et al.DPO: RLHF's KL optimum as a classifier loss

Cheat Sheet & Pitfalls REFERENCE

Formulas

H(p) = −Σ p log p

H(p,q) = −Σ p log q

KL = Σ p log(p/q)

H(p,q) = H(p) + KL

I(X;Y) = KL(pXY‖pXpY)

JS = ½KL(p‖m) + ½KL(q‖m)

∇z CE = softmax(z) − y

Which direction?

UseKL
MLE / supervisedKL(data‖model)
Variational inferenceKL(q‖posterior)
VAE regularizerKL(q(z|x)‖prior)
RLHF penaltyKL(π‖πref)
DistillationKL(teacher‖student)

Pitfalls

  • Calling KL a distance.
  • Forgetting that the CE floor is H(p), not 0.
  • Zero probabilities give infinite loss.
  • Unstable log(softmax).
  • Perplexities from different tokenizers.
  • Mixing bits and nats.
  • Swapped KL arguments in library calls.

Summary TAKEAWAYS

  1. A model is a code. Codeword length = −log q(x).
  2. Cross-entropy is the average length you pay with the wrong code.
  3. KL is the extra cost. Gibbs says it is ≥ 0, and 0 only when you are right.
  4. KL is asymmetric. Forward covers modes. Reverse picks one.
  5. MLE = min KL from the empirical distribution. So the classifier loss is cross-entropy.
  6. Softmax + CE gives the clean gradient q − y.
  7. MI, JS, ELBO, distillation and RLHF are all KL with different arguments.

One sentence

Learning is the search for the model that wastes the fewest bits on the data.

Further reading

  • Cover & Thomas, Elements of Information Theory, ch. 2
  • MacKay, Information Theory, Inference, and Learning Algorithms
  • Murphy, Probabilistic Machine Learning, ch. 6
  • Blei, Kucukelbir & McAuliffe, "Variational Inference: A Review for Statisticians" (2017)

Glossary REFERENCE

TermMeaning
Entropy H(p)Average surprise under p; best possible average code length
Cross-entropy H(p,q)Average code length when data come from p but you code with q
KL divergence KL(p‖q)Extra cost H(p,q) − H(p) ≥ 0; not symmetric
Gibbs' inequalityKL ≥ 0, with 0 only when p = q
Forward / reverse KLKL(data‖model) covers modes; KL(model‖data) picks one
Bit / natLog base 2 / base e; 1 nat ≈ 1.4427 bits
Perplexity2CE in bits: the "effective number of choices"
Mutual informationKL(p(x,y)‖p(x)p(y)): how far from independent
TermMeaning
LogitsRaw scores z before softmax
Softmaxezi / Σ ezj: turns logits into probabilities
MLEMaximum likelihood: same as minimizing KL(empirical‖model)
Label smoothingMix the one-hot target with uniform, weight ε
JS divergenceSymmetric average of KLs to the mixture; 0 to 1 bit
ELBOEvidence lower bound: log p(x) − KL(q‖posterior)
Temperature TDivide logits by T; higher T gives a flatter softmax
Log-sum-expStable way to compute log Σ ez: subtract the max first