The problem
Language models are trained on the logarithm of probabilities. If the model gave probability to the right token, the training loss is : close to 0 when is close to 1, and large when is small.
The obvious way, np.log(softmax(x)), breaks on very small probabilities. A probability such as is too small for float64 and becomes exactly 0, and then log(0) is -inf, even though the true answer, , is a perfectly ordinary number.
Example. For scores , the second probability is about .
np.log(softmax(x))returns[0, -inf]. The correct answer is[0, -800].
The fix is to never compute the tiny probability at all, and to work with logarithms the whole way. This needs one function, log-sum-exp.
Log-sum-exp
The LSE (log-sum-exp) of a list of scores is the log of the sum of their exponentials:
where is the list of scores (the logits).
Example. For : , and .
Computed this way it overflows, like softmax: for , is inf. The fix is to pull the largest score out of the sum first:
where is the largest score. After the subtraction the largest exponent is , so nothing overflows; and the sum contains that 1, so it is at least 1, and its log can never be .
Example. For : , the shifted scores are , their exponentials add up to , so .
For the naive formula has the opposite problem: is
0, so it computes . With the shift, , the sum is again 2, and the result is .
Log-softmax
The log of the softmax can be written with LSE, with no division and no tiny probability in between:
where is score . The log of a fraction is the log of the top minus the log of the bottom, and .
Example. For , , so the log-softmax is . These are the logs of the probabilities .
For , is essentially , and the log-softmax is
[0, -800], the right answer.
What to implement
logsumexp(x, axis=-1, keepdims=False): the LSE alongaxis. Likenp.sum, it removes that axis from the shape, unlesskeepdims=True, in which case the axis stays with size 1. For a table of shape(4, 6)andaxis=-1, the result has shape(4,), or(4, 1)withkeepdims=True: one value per row.log_softmax(x, axis=-1): the log-softmax alongaxis, with the same shape asx.
Requirements
- Stable for very large and very negative scores:
logsumexp([1000, 1000])is andlogsumexp([-1000, -1000])is . - Works when some scores are
-inf. This is how attention hides positions it must not look at: , so they add nothing to the sum. Inlog_softmax, a-infscore stays-inf. - No Python loops.
Hint
- Compute the maximum with
keepdims=True, so it lines up withxwhen you subtract it, then remove the axis at the end ifkeepdimsisFalse(np.squeeze). log_softmaxcan call yourlogsumexp: withkeepdims=Trueits result lines up withx.