Softmax Revisited
🏷️subsec_softmax-implementation-revisited
In :numref:sec_softmax_scratch we calculated our model's output
and applied the cross-entropy loss. While this is perfectly
reasonable mathematically, it is risky computationally, because of
numerical underflow and overflow in the exponentiation.
Recall that the softmax function computes probabilities via
\hat y_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}.
If some of the o_k are very large, i.e., very positive,
then \exp(o_k) might be larger than the largest number
we can have for certain data types. This is called overflow. Likewise,
if every argument is a very large negative number, we will get underflow.
For instance, single precision floating point numbers approximately
cover the range of 10^{-38} to 10^{38}. As such, if the largest term in \mathbf{o}
lies outside the interval [-90, 90], the result will not be stable.
A way round this problem is to subtract \bar{o} \stackrel{\textrm{def}}{=} \max_k o_k from
all entries:
\hat y_j = \frac{\exp o_j}{\sum_k \exp o_k} =
\frac{\exp(o_j - \bar{o}) \exp \bar{o}}{\sum_k \exp (o_k - \bar{o}) \exp \bar{o}} =
\frac{\exp(o_j - \bar{o})}{\sum_k \exp (o_k - \bar{o})}.
By construction we know that o_j - \bar{o} \leq 0 for all j. As such, for a $q$-class
classification problem, the denominator is contained in the interval [1, q]. Moreover, the
numerator never exceeds 1, thus preventing numerical overflow. Numerical underflow only
occurs when \exp(o_j - \bar{o}) numerically evaluates as 0. Nonetheless, a few steps down
the road we might find ourselves in trouble when we want to compute \log \hat{y}_j as \log 0.
In particular, in backpropagation,
we might find ourselves faced with a screenful
of the dreaded NaN (Not a Number) results.
Fortunately, we are saved by the fact that
even though we are computing exponential functions,
we ultimately intend to take their log
(when calculating the cross-entropy loss).
By combining softmax and cross-entropy,
we can escape the numerical stability issues altogether. We have:
\log \hat{y}_j =
\log \frac{\exp(o_j - \bar{o})}{\sum_k \exp (o_k - \bar{o})} =
o_j - \bar{o} - \log \sum_k \exp (o_k - \bar{o}).
This avoids both overflow and underflow.
We will want to keep the conventional softmax function handy
in case we ever want to evaluate the output probabilities by our model.
But instead of passing softmax probabilities into our new loss function,
we just
[pass the logits and compute the softmax and its log
all at once inside the cross-entropy loss function,]
which does smart things like the "LogSumExp trick".