Float32 spans roughly 10^{-38} to 10^{38}: \expoverflows to \infty once its argument passes \approx +88, and past \approx -88 it gradually underflows through the subnormals, hitting exactly 0 near -104.
Feed the from-scratch softmax the logits \mathbf{o}=(1000, 0, 0): \exp(1000)=\infty, the ratio is \infty/\infty=NaN, and a NaN propagates through the backward pass. The example in the softmax-from-scratch section; the fused loss below never forms that ratio.
Fix, step 1: shift by the max
Numerical stability
Softmax is unchanged if we subtract the same constant from every logit (the \exp\bar{o} factors cancel). Choose \bar{o}=\max_k o_k:
Now every exponent o_j - \bar{o} \le 0, so each \exp lands in (0, 1]: no overflow. The denominator sits in [1, q].
Fix, step 2: never form the softmax
Numerical stability
Underflow remains a problem if we then take \log of a near-zero probability. But we only ever want \log \hat y_j for the loss, so fold the \log in and the division disappears:
\log\sum_k\exp(o_k) is a smooth upper bound on \max_k o_k, which motivates the name soft maximum.
Its gradient is \partial_{o_j}\ell = \mathrm{softmax}(\mathbf{o})_j - y_j, with no clamp to perturb it.
The soft maximum differs by at most log 2
Numerical stability · measured
For two classes with logits (x, 0) the loss’s first term is \mathrm{lse}(x, 0) = \log(1 + e^x). Plot it against \max(x, 0):
The gap peaks at the tiex = 0, where it equals \log 2 \approx 0.69, the bound \log q you proved in the softmax-regression section (exercise 6), here at q = 2. The gap decreases away from the tie.
03
In code
one fused call, four frameworks
Pass logits directly to the loss
The fused loss
PyTorch’s F.cross_entropy consumes logits by definition: it computes the stable log-sum-exp internally. We attach it to the base Classifier, so every classifier in the book inherits it:
@d2l.add_to_class(d2l.Classifier)def loss(self, Y_hat, Y, averaged=True): Y_hat = d2l.reshape(Y_hat, (-1, Y_hat.shape[-1])) Y = d2l.reshape(Y, (-1,))return F.cross_entropy( Y_hat, Y, reduction='mean'if averaged else'none')
The fused-loss interface
The fused loss
The name differs by library; the interface does not. The built-in fused loss takes logits, not probabilities: passing softmax outputs would softmax twice.
Defined once on Classifier (note the #@save): the whole book inherits the stable loss.
04
Train
same linear model with framework components
Train
Results
Same Fashion-MNIST, same 10 epochs, same Trainer:
data = d2l.FashionMNIST(batch_size=256)model = SoftmaxRegression(num_outputs=10, lr=0.1)trainer = d2l.Trainer(max_epochs=10)trainer.fit(model, data)
In the displayed run, validation accuracy is ~83–84%, close to the from-scratch model in the softmax-from-scratch section. The implementation uses a stable fused loss rather than explicit softmax followed by clipping.
Recap
Wrap-up
From scratch taught what softmax and cross-entropy are; concise uses the stable framework components typical of applications.
The forward pass outputs logits; the built-in loss owns the softmax.
That built-in is the log-sum-exp rewrite \ell = \bar{o} + \log\sum_k e^{o_k-\bar{o}} - o_y, not a naive softmax → log → NLL.
lse is a smooth max: within \log q of \max_k o_k, gap largest (\log 2 for q{=}2) exactly at the tie.
The fused expression avoids the float32 \pm 88 (and -104) thresholds by evaluating the stable log-sum-exp form directly.