How many extra bits do you pay for believing the wrong thing?
That one number trains almost every modern model.
Average code length when you code with the wrong model.
The extra bits. Never negative. Zero only when you are right.
Max likelihood = min KL. Softmax loss = cross-entropy.
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+).
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)
| x | p(x) | −log2p | ideal code |
|---|---|---|---|
| A | 1/2 | 1 | 0 |
| B | 1/4 | 2 | 10 |
| C | 1/8 | 3 | 110 |
| D | 1/8 | 3 | 111 |
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 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.
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.
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.
| Property | Holds? |
|---|---|
| KL(p‖q) ≥ 0 | yes (Gibbs) |
| KL(p‖q) = 0 ⇔ p = q | yes |
| Symmetric | no |
| Triangle inequality | no |
| Convex in the pair (p, q) | yes |
| Additive for independent pairs | yes |
| Invariant under invertible maps of x | yes |
| Can be infinite | yes |
It is a divergence, not a distance. It fails symmetry and the triangle inequality. Its square root is not a metric either.
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. ∎
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
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.
H(p,q) = −Σ p log q
= −Σ p log p + Σ p log (p/q)
= H(p) + KL(p‖q) ∎
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.
The assert in the cross-entropy snippet checks this identity numerically. Try it yourself on a few random pairs.
True weather p and a forecaster's model q over sun, cloud, rain. All logs base 2.
| x | p | q | p·(−log p) | p·(−log q) | p·log(p/q) |
|---|---|---|---|---|---|
| sun | 1/2 | 1/4 | 0.5 | 1.0 | +0.5 |
| cloud | 1/4 | 1/4 | 0.5 | 0.5 | 0 |
| rain | 1/4 | 1/2 | 0.5 | 0.25 | −0.25 |
| column sums | H(p) = 1.5 | H(p,q) = 1.75 | KL = 0.25 | ||
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.
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.
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(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
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.
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.
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.
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.
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) |
|---|---|---|
| 0 | anything | 0 (by convention) |
| > 0 | > 0 | finite |
| > 0 | 0 | +∞ |
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.
Data x1, …, xN. The empirical distribution puts mass 1/N on each sample:
p̂(x) = #{i : xi = x}⁄N
−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.
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
∂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]
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
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.5 | 0.50 | 0.25 |
| 0.1 | 0.90 | 0.162 |
| 0.01 | 0.99 | 0.0196 |
| 0.0001 | 0.9999 | 0.0002 |
MSE is still right for regression with Gaussian noise. That is MLE too, just with a different model.
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.
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.
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
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 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.
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
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 t | KL(p‖q) |
| −log t | KL(q‖p) |
| (t − 1)² | Pearson χ² |
| ½|t − 1| | total variation |
| (√t − 1)² | squared Hellinger (×2) |
| t log t − (t+1) logt+1⁄2 | 2 · 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)
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.
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. ∎
JS = 1.0000 bits on the last slide.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)).
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 = 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.
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).
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.
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) )
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.
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 )
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.
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
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).KLDivLoss in PyTorch expects log-probs for the input and probs for the target. The argument order is (q, p), which confuses many.Stay in log-space as long as you can. Exponentiate only at the very end, if ever.
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(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).
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.
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.
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.
cross_entropy(logits, y) applies softmax itself. Passing probabilities applies it twice. The loss still falls, but the gradients are wrong.
Try each one before you look to the right.
| Year | Who | What |
|---|---|---|
| 1902 | J. W. Gibbs | The inequality, in statistical mechanics |
| 1906 | J. Jensen | Jensen's inequality for convex functions |
| 1948 | C. Shannon | Entropy, source coding, the bit |
| 1951 | S. Kullback & R. Leibler | "On Information and Sufficiency": the divergence we now call KL |
| 1957 | E. T. Jaynes | Maximum entropy principle |
| 1963–66 | I. Csiszár; Ali & Silvey | f-divergences |
| 1973 | H. Akaike | AIC: model selection as estimated KL |
| 1991 | J. Lin | Jensen–Shannon divergence |
| 1999 | Jordan et al. | Variational methods for graphical models |
| 2013 | Kingma & Welling | Variational autoencoders |
| 2014 | Goodfellow et al. | GANs and the JS connection |
| 2015 | Hinton, Vinyals, Dean | Knowledge distillation |
| 2015–16 | Szegedy et al. | Label smoothing (Inception v3) |
| 2017–22 | Christiano; Ouyang et al. | RLHF with a KL penalty (InstructGPT) |
| 2023 | Rafailov et al. | DPO: RLHF's KL optimum as a classifier loss |
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
| Use | KL |
|---|---|
| MLE / supervised | KL(data‖model) |
| Variational inference | KL(q‖posterior) |
| VAE regularizer | KL(q(z|x)‖prior) |
| RLHF penalty | KL(π‖πref) |
| Distillation | KL(teacher‖student) |
log(softmax).Learning is the search for the model that wastes the fewest bits on the data.
| Term | Meaning |
|---|---|
| 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' inequality | KL ≥ 0, with 0 only when p = q |
| Forward / reverse KL | KL(data‖model) covers modes; KL(model‖data) picks one |
| Bit / nat | Log base 2 / base e; 1 nat ≈ 1.4427 bits |
| Perplexity | 2CE in bits: the "effective number of choices" |
| Mutual information | KL(p(x,y)‖p(x)p(y)): how far from independent |
| Term | Meaning |
|---|---|
| Logits | Raw scores z before softmax |
| Softmax | ezi / Σ ezj: turns logits into probabilities |
| MLE | Maximum likelihood: same as minimizing KL(empirical‖model) |
| Label smoothing | Mix the one-hot target with uniform, weight ε |
| JS divergence | Symmetric average of KLs to the mixture; 0 to 1 bit |
| ELBO | Evidence lower bound: log p(x) − KL(q‖posterior) |
| Temperature T | Divide logits by T; higher T gives a flatter softmax |
| Log-sum-exp | Stable way to compute log Σ ez: subtract the max first |