Skip to content

← Reductions and dim step 3 of 7

Medium Primitives

Softmax from the definition

Jump to the task

Softmax turns a row of scores into probabilities:

softmax(x)_i = exp(x_i) / sum_j exp(x_j)

Evaluating this formula directly can overflow even when the probabilities are well defined:

import torch

x = torch.tensor([[1000.0, 1001.0]], dtype=torch.float32)
e = x.exp()
print(e.tolist())  # [[inf, inf]]
print((e / e.sum(dim=-1, keepdim=True)).tolist())  # [[nan, nan]]

The exponentials exceed float32's finite range. Dividing infinity by infinity then produces NaN. Very negative inputs cause a different failure: all exponentials can round to zero, leaving a zero denominator.

Shift before exponentiating

Subtract the maximum of each row before applying exp:

z = x - x.max(dim=-1, keepdim=True).values
e = z.exp()
probabilities = e / e.sum(dim=-1, keepdim=True)
print([round(v, 6) for v in probabilities[0].tolist()])
# [0.268941, 0.731059]

For real-valued arithmetic, subtracting the same constant from every score does not change softmax. The numerator and denominator both acquire the same factor, which cancels.

For finite inputs, subtracting the maximum makes the largest shifted score zero. Its exponential is one, and the other exponentials are at most one. This prevents large positive scores from overflowing during exponentiation and prevents every exponential from underflowing to zero.

Floating-point rounding still applies. Very small probabilities can become zero, and the shift is not a promise of exact output or correct handling of NaNs and infinities. Low-precision sums and extremely wide rows can require additional care. This exercise uses small rows of finite float32 scores.

Preserve the reduced dimension

x.max(dim=-1, keepdim=True) returns values and indices; use its .values for subtraction. Keeping the last dimension gives one maximum per row in a shape that broadcasts back across that row. Keep the dimension when summing exponentials for the same reason.

In application code, use torch.softmax(x, dim=-1). For log-probabilities, use log_softmax rather than taking the logarithm of probabilities that may have rounded to zero. Implementing the shift here helps explain the numerical-stability idea also used by log-sum-exp.

Your task

def softmax(x: torch.Tensor) -> torch.Tensor

Return softmax over the last axis of the supplied two-dimensional float32 tensor. Preserve its shape and handle large positive and negative finite scores. Implement the reduction yourself without calling torch.softmax, torch.nn.functional.softmax or logsumexp.