Both facts follow from the exponent rule e^(a+b) = e^a · e^b. For the shift:
softmax(z − m)ᵢ = e^(zᵢ − m) / Σⱼ e^(zⱼ − m)
= e^(zᵢ)·e^(−m) / (e^(−m)·Σⱼ e^(zⱼ))
= e^(zᵢ) / Σⱼ e^(zⱼ) ✓ identical
numeric check, z = [100, 101, 102], m = 102:
e^(z−m) = [0.135335, 0.367879, 1.000000]
Σ = 1.503214
probs = [0.090030, 0.244728, 0.665241]
sum of probs = 1.000000 ✓
Log-sum-exp is the same trick seen through a logarithm. It is the quantity inside every log-probability and every cross-entropy:
log Σᵢ e^(xᵢ)
= log Σᵢ e^(xᵢ − c + c) add and subtract c
= log Σᵢ e^(xᵢ − c)·e^c e^(a+b) = e^a·e^b
= log [ e^c · Σᵢ e^(xᵢ − c) ] factor e^c out of the sum
= c + log Σᵢ e^(xᵢ − c) log(a·b) = log a + log b
choose c = max(x):
largest term is e⁰ = 1 → no overflow, ever
at least one term is exactly 1 → Σ ≥ 1, so log never sees 0
numeric check, x = [500, 501, 502]:
naive float32: e⁵⁰⁰ overflows → inf
stable: 502 + log(1 + e⁻¹ + e⁻²)
= 502 + log(1.5032147)
= 502.407606
cross-entropy check, z = [2, 5, 1], true class 0:
loss = logsumexp(z) − z₀ = 5.0658877 − 2 = 3.0658877 nats
The stable form is not an optimization; it is a requirement for correctness. Frameworks fuse softmax and cross-entropy so that the loss is computed from logits with log-sum-exp inside.
The three functions, exactly as the lesson builds thempython
import math
def softmax_stable(z):
m = max(z) # largest logit
e = [math.exp(v - m) for v in z] # largest exponent is 0
s = sum(e)
return [x / s for x in e]
def logsumexp(z):
c = max(z) # factor out e^c
return c + math.log(sum(math.exp(v - c) for v in z))
def cross_entropy_stable(true_class, z):
return logsumexp(z) - z[true_class] # −log softmax(z)[t]
Every value in this chapter was recomputed from these formulas; the labs run the same arithmetic in your browser.