AI Knowledge Tree

The log-sum-exp trick and stable softmax

Derive the log-sum-exp shift and use it to compute softmax and log-likelihoods without overflow.

⏱ 28 minPrerequisites:Catastrophic cancellation

The softmax and the log-likelihood of a classifier both involve log⁡∑iexi\log \sum_i e^{x_i}, which overflows for any moderately large xix_i and underflows for moderately negative ones[1]. The log-sum-exp trick is a one-line algebraic identity that makes the expression stable for any inputs. Every deep-learning framework uses it internally.

The naive failure

Computing ∑iexi\sum_i e^{x_i} with float32: at x=100x = 100, ex≈2.7×1043e^x \approx 2.7 \times 10^{43}, still finite, but x=1000x = 1000 gives ∞\infty. Even before overflow, summing one huge exie^{x_i} with many tiny exje^{x_j} loses the small terms entirely. Softmax, softmax(x)i=exi/∑jexj\text{softmax}(x)_i = e^{x_i}/\sum_j e^{x_j}, returns NaN\text{NaN} or 00 in those regimes.

The shift identity

For any constant cc,

log⁡∑i=1nexi=c+log⁡∑i=1nexi−c.\log \sum_{i=1}^n e^{x_i} = c + \log \sum_{i=1}^n e^{x_i - c}.

Derivation. Factor ece^c out of the sum: ∑iexi=ec∑iexi−c\sum_i e^{x_i} = e^c \sum_i e^{x_i - c}. Taking the log, log⁡∑iexi=c+log⁡∑iexi−c\log\sum_i e^{x_i} = c + \log\sum_i e^{x_i - c}, where the right side uses log⁡(ab)=log⁡a+log⁡b\log(ab) = \log a + \log b. ■\qquad\blacksquare

The identity holds for any cc; the smart choice is c=max⁡ixic = \max_i x_i. Then xi−c≤0x_i - c \le 0 for all ii, so each exi−c∈(0,1]e^{x_i - c} \in (0, 1], no term overflows, and at least one term equals 1 (avoiding underflow of the whole sum to zero). The shifted formula is numerically stable for any input[2].

Stable softmax

The same shift carries through to softmax. Subtracting cc from every xix_i multiplies numerator and denominator by the same e−ce^{-c}, leaving the ratio unchanged:

softmax(x)i=exi−c∑jexj−c.\text{softmax}(x)_i = \frac{e^{x_i - c}}{\sum_j e^{x_j - c}}.

With c=max⁡ixic = \max_i x_i, both numerator and denominator are well-scaled.

Log-softmax and the cross-entropy loss

Training computes the log-softmax for the true class, not the softmax itself. Computing log⁡softmax(x)y=xy−log⁡∑iexi\log \text{softmax}(x)_y = x_y - \log\sum_i e^{x_i} directly applies the shift:

log⁡softmax(x)y=(xy−c)−log⁡∑iexi−c.\log \text{softmax}(x)_y = (x_y - c) - \log \sum_i e^{x_i - c}.

This avoids the lossy two-step "softmax then log", which can produce log⁡0=−∞\log 0 = -\infty when a class has probability that underflowed. The fused log_softmax is the preferred building block, and the cross-entropy loss with logits in deep learning calls it internally.

Computing log Σ exp(xᵢ) directly overflows once the inputs are large; subtracting the maximum first keeps it stable for any magnitude, with an identical result.
Computing log Σ exp(xᵢ) directly overflows once the inputs are large; subtracting the maximum first keeps it stable for any magnitude, with an identical result.

Check it on arrays

python · numpy
import numpy as np

def logsumexp_naive(x):
    return np.log(np.sum(np.exp(x)))

def logsumexp_stable(x):
    c = np.max(x)
    return c + np.log(np.sum(np.exp(x - c)))

def softmax_naive(x):
    e = np.exp(x)
    return e / e.sum()

def softmax_stable(x):
    e = np.exp(x - x.max())
    return e / e.sum()

# Overflow regime
x_big = np.array([1000.0, 1001.0, 1002.0])
print(f"naive   logsumexp: {logsumexp_naive(x_big):.6f}")     # inf
print(f"stable  logsumexp: {logsumexp_stable(x_big):.6f}")     # ~1002.407...
print(f"naive   softmax:   {softmax_naive(x_big)}")            # nan
print(f"stable  softmax:   {softmax_stable(x_big)}")           # finite

# Underflow regime
x_small = np.array([-1000.0, -1001.0, -1002.0])
print(f"naive   softmax:   {softmax_naive(x_small)}")          # 0/0 -> nan
print(f"stable  softmax:   {softmax_stable(x_small)}")         # correct

# Verify the stable softmax matches the analytic answer
x = np.array([1.0, 2.0, 3.0])
analytic = np.exp(x) / np.exp(x).sum()
print("agreement at moderate x:", np.allclose(softmax_stable(x), analytic))

# Compare against numpy's library implementation
print("matches np.exp normalization:", np.allclose(softmax_stable(x_big), softmax_stable(x_big - 500)))

The naive computations break in both extremes while the shifted versions return finite, correct answers. Shifting by the max is the entire fix, and the cost is one extra pass over the array.

Exercises

Exercise 1. Use the shift identity with c=max⁡ixic = \max_i x_i to compute log⁡(e1000+e1001+e1002)\log(e^{1000} + e^{1001} + e^{1002}) by hand, expressing the answer as 1002+log⁡(e−2+e−1+1)1002 + \log(e^{-2} + e^{-1} + 1) and giving its numerical value.

Solution

Step 1. The maximum is c=1002c = 1002. The identity log⁡∑iexi=c+log⁡∑iexi−c\log \sum_i e^{x_i} = c + \log \sum_i e^{x_i - c} gives

log⁡(e1000+e1001+e1002)=1002+log⁡ ⁣(e−2+e−1+e0).\log(e^{1000} + e^{1001} + e^{1002}) = 1002 + \log\!\left(e^{-2} + e^{-1} + e^{0}\right).

Step 2. Evaluate the shifted sum: e−2≈0.1353e^{-2} \approx 0.1353, e−1≈0.3679e^{-1} \approx 0.3679, e0=1e^{0} = 1, so the sum is ≈1.5032\approx 1.5032 and log⁡(1.5032)≈0.4076\log(1.5032) \approx 0.4076.

Step 3. The result is 1002+0.4076=1002.40761002 + 0.4076 = 1002.4076. Every shifted exponent is ≤0\le 0, so no term overflows, and the term for ii at the maximum equals 11, keeping the sum away from underflow.

Exercise 2. Prove the bound max⁡ixi≤log⁡∑iexi≤max⁡ixi+log⁡n\max_i x_i \le \log \sum_i e^{x_i} \le \max_i x_i + \log n for nn terms. This is why log-sum-exp is sometimes called a smooth maximum.

Solution

Step 1. Let c=max⁡ixic = \max_i x_i. Every term exi>0e^{x_i} > 0, and one term equals ece^c, so ∑iexi≥ec\sum_i e^{x_i} \ge e^c. Taking logs, log⁡∑iexi≥c\log \sum_i e^{x_i} \ge c, the lower bound.

Step 2. Each term satisfies exi≤ece^{x_i} \le e^c, so ∑iexi≤n ec\sum_i e^{x_i} \le n\, e^c. Taking logs, log⁡∑iexi≤log⁡(n ec)=c+log⁡n\log \sum_i e^{x_i} \le \log(n\, e^c) = c + \log n, the upper bound.

Step 3. Together, c≤log⁡∑iexi≤c+log⁡nc \le \log \sum_i e^{x_i} \le c + \log n: log-sum-exp returns the maximum plus a slack that never exceeds log⁡n\log n, and the slack vanishes when one term dominates the rest.

Exercise 3 (code). Implement stable log_softmax directly as (xy−c)−log⁡∑iexi−c(x_y - c) - \log \sum_i e^{x_i - c} with c=max⁡ixic = \max_i x_i. Verify it agrees with the two-step log⁡(softmax(x))\log(\text{softmax}(x)) on moderate inputs and stays finite on inputs that make the two-step route return −∞-\infty.

Solution
python · numpy
import numpy as np

def log_softmax_stable(x):
    c = x.max()
    shifted = x - c
    return shifted - np.log(np.sum(np.exp(shifted)))

def log_softmax_twostep(x):
    e = np.exp(x)
    return np.log(e / e.sum())          # underflows then logs zero

# Moderate inputs: both routes agree
x = np.array([1.0, 2.0, 3.0])
assert np.allclose(log_softmax_stable(x), log_softmax_twostep(x))

# Large inputs: two-step route produces -inf, stable route stays finite
x_big = np.array([1000.0, 1001.0, 1002.0])
ls = log_softmax_stable(x_big)
assert np.all(np.isfinite(ls))
assert not np.all(np.isfinite(log_softmax_twostep(x_big)))
print("stable log_softmax:", ls)

The direct form subtracts the max before exponentiating, so it never overflows; fusing the log into the subtraction also avoids the log⁡0=−∞\log 0 = -\infty the two-step route hits once a class probability underflows.

The next lesson moves from elementwise numerics to solving systems by repeated approximation.


References

  1. [1]Goldberg, D. (1991). "What every computer scientist should know about floating-point arithmetic". ACM Computing Surveys, 23(1), 5-48. Overflow, underflow, and the exponential range (doi:10.1145/103162.103163).↩
  2. [2]Higham, N. J. (2002). Accuracy and Stability of Numerical Algorithms (2nd ed.). SIAM. Reformulation to avoid overflow in exponential sums (doi:10.1137/1.9780898718027).↩