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:
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
Optax names it for exactly what it does: softmax_cross_entropy_with_integer_labels takes logits plus integer labels and fuses the stable softmax with the cross-entropy:
@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,)) fn = optax.softmax_cross_entropy_with_integer_labelsreturn fn(Y_hat, Y).mean() if averaged else fn(Y_hat, Y)
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.