Softmax Regression
One score per class, exponentiated then divided by their total. The classes now compete for a fixed budget of one, so only the gaps between them matter.
Why Does This Exist?
Every incoming support ticket goes to one of four queues: billing, bug, feature request, account access. One ticket, one queue, and you want four numbers saying how likely each is.
That's the case for this page, and it's the shape of nearly every classifier you'll ever ship. The last layer of a neural-network classifier is exactly this. ImageNet's thousand classes, a language model's next-token distribution over 100,000 tokens: same arithmetic, just wider.
You know the two-class version. Logistic regression computes one score, squashes it through a sigmoid, and reads the result as the probability of "yes". The obvious move for four queues is four separate logistic models. Run it and the answers don't add up, literally: 0.7, 0.6, 0.2, 0.4 sums to 1.9, with no rule for turning that into a decision. Nothing told the four models they were rivals.
Softmax is the fix. Give each class its own score, exponentiate so every number is positive, divide by the total. Now they sum to one, and pushing one up pushes the others down. Those raw pre-softmax scores are logits, a word you'll meet in every library doc.
Think of It Like This
One bag of flour, four loaves
Four bakers, one bag of flour, and the whole bag gets used. Give the bug queue more and something else gets less. There's no way to be generous twice.
Each baker's share depends on how loudly they argue, on a scale with no ceiling. Here's what makes softmax softmax: the arguments get amplified before the split, so a two-point lead doesn't buy slightly more flour, it buys a lot more. Score 4 against score 2 becomes a 7-to-1 share, not 2-to-1. And if every baker gets five points louder the split is identical, because the relative arguments haven't moved. Only differences reach the flour.
How It Actually Works
Score every class, then take shares
One weight vector per class, so a ticket with queues gets logits:
is the weight vector for class and its intercept. Each is unbounded, and they aren't comparable yet. Softmax makes them comparable:
The numerator is positive whatever was. The denominator is shared, so the ranking survives untouched and the four values now sum to exactly 1. With the whole thing collapses back to the sigmoid, since — logistic regression was always the two-class case.
Shifts are free, and that's not a curiosity
Subtract any constant from all four logits and the answer doesn't move:
The cancels top and bottom. Softmax reads only the gaps between logits, never their level.
Every serious implementation cashes that in. Double-precision exp overflows to infinity north of an input of 709, and a logit of 800 isn't exotic in an unregularised model or a network mid-training. Feed it in directly and you get inf over inf, so nan out, poisoning every gradient downstream. Subtract the largest logit first and the biggest input to exp is 0, while shift-invariance guarantees the answer is unchanged. That one subtraction is the whole of what people mean by a "numerically stable softmax".
One spare degree of freedom, and a gradient you've seen
Because only gaps matter, adding the same vector to all four weight rows leaves predictions bit-for-bit identical. The parameters are over-determined: rows describe a function rows already pin down. Nothing breaks, but the coefficients aren't unique, so "the weight on billing" means nothing on its own. Some libraries remove the slack by fixing one class's row at zero, making every remaining coefficient a comparison against that reference class.
The loss is categorical cross-entropy, with the true queue. Only the true class's probability enters, because the one-hot label zeroes out the rest. The gradient is the one from the two-class page, now per class:
Predicted minus actual. Chain it through the linear layer and the weight gradient is . That's why the two always ship together: the derivatives cancel against the and you're left with subtraction.
Show Me the Code
Four logits for one ticket, the stable version, then the naive version failing.
import numpy as np
def softmax(z: np.ndarray) -> np.ndarray: z = z - z.max() # shift-invariant, so this is free and it stops exp from overflowing e = np.exp(z) return e / e.sum()
scores = np.array([2.0, 4.0, 1.0, 3.0]) # billing, bug, feature, accessprint(np.round(softmax(scores), 3))print(np.round(softmax(scores + 800.0), 3)) # same gaps, so the same answerwith np.errstate(over="ignore", invalid="ignore"): raw = np.exp(scores + 800.0) # what the textbook formula does, unshifted print(raw / raw.sum())# -> [0.087 0.644 0.032 0.237]# -> [0.087 0.644 0.032 0.237]# -> [nan nan nan nan]A one-point lead, 4 against 3, becomes 0.644 against 0.237, nearly three to one. Shift all four by 800 and the stable version doesn't blink; the unshifted one returns four nans.
Watch Out For
Exponentiating raw logits straight into nan
Training runs fine for a while, then the loss prints nan and every metric flatlines. The usual cause is a hand-rolled softmax hit by a logit large enough to overflow exp, and it doesn't take a bug to get there: remove weight decay, let the learning rate run hot, or feed an unscaled feature, and logits in the high hundreds turn up on their own. One bad row in a batch is enough, so it looks intermittent and people chase the data instead of the arithmetic.
Subtract the max, or better, never write the softmax at all. Every framework ships a fused loss that takes logits and does the stable computation internally: PyTorch's CrossEntropyLoss, TensorFlow's from_logits=True. The mirror-image mistake is quieter — applying your own softmax, then handing the probabilities to one of those fused losses. Nothing errors, gradients shrink toward nothing, and the model plateaus early with no clue why.
Using softmax where labels can co-occur
A ticket says the app crashed and the customer was double-charged. Both queues are correct, and softmax cannot say so, because the shares must sum to one: the only way to raise billing is to take probability away from bug. In the metrics that reads as decent top-1 accuracy beside terrible recall on second labels.
The problem isn't the model, it's the question. "Which one of these?" is softmax. "Which of these apply?" is independent sigmoids with binary cross-entropy on each and a threshold per label, so two labels can both sit at 0.9. Tagging and content moderation are both the second kind, and the bug survives review because a softmax model on multi-label data still returns a plausible-looking answer.
The Quick Version
- One logit per class, exponentiate, divide by the total. Positive, sums to one, ranking preserved.
- Logits are the raw pre-softmax scores, and library APIs care which of the two you hand them.
- Softmax reads only the gaps between logits, which is why you subtract the max before exponentiating.
- Skip that and large logits overflow to
inf, thennan, then every weight downstream. - One redundant degree of freedom, so a class's coefficients need a reference class to mean anything.
- Paired with cross-entropy the gradient is , predicted minus actual, per class.
- Mutually exclusive labels want softmax. Co-occurring labels want independent sigmoids.
What to Read Next
- Activation Functions places softmax among its neighbours and explains why it belongs only at the output.
- Cross-Entropy is where the loss comes from, and why pairing it with softmax gives such a clean gradient.
- Multi-Layer Perceptron is this same output layer with learned features underneath instead of raw columns.
- Classification Metrics handles the multiclass bookkeeping: macro against micro averaging.
- Definitions to keep nearby: Softmax and Log-Sum-Exp.