skip to content

Why does the gradient of softmax cross-entropy with respect to the logits reduce to p - y?

level: middleimportance: must knowfreq 68%

answer

  1. the log's reciprocal meets the Jacobian's leading factor
  2. dp_k/dz_m = p_k * (delta_km - p_m)
  3. one clean subtraction per logit
  4. components sum to zero, each within [-1, 1]

basics

~20 s

The softmax derivative cancels against the derivative of the log instead of multiplying into it. Differentiating -log of the correct class's probability through the softmax leaves predicted probability minus one-hot target, one subtraction per logit, bounded between -1 and 1.

solid answer

~50 s

Write `p_k = exp(z_k) / sum_j exp(z_j)` and `L = -log p_c` for correct class `c`. The softmax Jacobian is `dp_k/dz_m = p_k * (delta_km - p_m)`. The loss depends on the logits only through `p_c`, so `dL/dz_m = -(1/p_c) * dp_c/dz_m = -(1/p_c) * p_c * (delta_cm - p_m) = p_m - delta_cm`. The `1/p_c` from the log exactly cancels the leading `p_c` in the Jacobian, which is why nothing saturating survives. In vector form `dL/dz = p - y` with `y` the one-hot target. Every component is in `[-1, 1]`: the correct logit gets `p_c - 1`, each wrong logit gets `+p_m`. A confidently wrong example therefore produces the largest gradient the loss can make, magnitude close to one, and a confidently right one produces almost nothing. Averaging over a batch divides by the batch size.

code

python · 17 lines
python
import math

logits = [2.0, 1.0, 0.1]        # raw scores for three classes
target = 0                       # index of the correct class

exps = [math.exp(z) for z in logits]
total = sum(exps)
p = [e / total for e in exps]                   # softmax probabilities

loss = -math.log(p[target])                     # categorical cross-entropy
grad = [p[k] - (1.0 if k == target else 0.0)    # the fused gradient p - y
        for k in range(len(logits))]

print([round(v, 4) for v in p])      # [0.659, 0.2424, 0.0986]
print(round(loss, 4))                # 0.417
print([round(g, 4) for g in grad])   # [-0.341, 0.2424, 0.0986]
print(round(sum(grad), 12))          # 0.0  -> the components cancel

go deeper

for a junior

Be able to state the result and read it: the correct class's component is negative, every other class's is its own predicted probability, and the whole thing shrinks as the model gets the example right.

for a middle

Expect to derive it at a whiteboard. Know the softmax Jacobian p_k * (delta_km - p_m), show the 1/p_c from the logarithm cancelling its leading factor, and say why nothing saturating survives.

for a senior

Demonstrate the debugging value: components that fail to sum to zero, or an update off by a factor of p * (1 - p), point straight at a double-applied softmax derivative. Know that soft targets leave the form untouched.

for a principal

Own the general lesson for objective design - pair an output activation with the loss whose derivative cancels it, and prefer objectives whose gradient magnitude grows monotonically with how wrong the prediction is.

## Setting it up A classification head produces `K` unbounded real scores called **logits**, `z_1 ... z_K`. The softmax turns them into probabilities: ``` p_k = exp(z_k) / sum_j exp(z_j) ``` The categorical cross-entropy loss for one example whose true class is `c` is ``` L = -log p_c ``` The question is what `dL/dz_m` looks like - the sensitivity of the loss to each raw score, which is what training actually needs. ## The softmax Jacobian Softmax maps a vector to a vector, so its derivative is a `K x K` matrix. Two cases: - **Same index** (`k = m`): `dp_k/dz_k = p_k * (1 - p_k)`. - **Different index** (`k != m`): `dp_k/dz_m = -p_k * p_m`. Both collapse into one expression using the indicator `delta_km`, which is 1 when `k = m` and 0 otherwise: ``` dp_k/dz_m = p_k * (delta_km - p_m) ``` The off-diagonal term is negative and it is the whole reason the classes compete: raising any logit pushes every other probability down. ## The cancellation The loss touches the logits only through `p_c`, so the chain rule has a single term: ``` dL/dz_m = dL/dp_c * dp_c/dz_m ``` The first factor is `d(-log p_c)/dp_c = -1/p_c`. The second is the Jacobian entry `p_c * (delta_cm - p_m)`. Multiply: ``` dL/dz_m = -(1/p_c) * p_c * (delta_cm - p_m) = -(delta_cm - p_m) = p_m - delta_cm ``` The `p_c` in the numerator of the Jacobian and the `1/p_c` from the logarithm annihilate each other. Since `delta_cm` is exactly the one-hot target vector `y`, the whole gradient is ``` dL/dz = p - y ``` That cancellation is the point of the whole derivation. The Jacobian factor `p_c` is the quantity that becomes vanishingly small when the model is badly wrong; the logarithm's `1/p_c` is exactly its reciprocal. Compose them and the dangerous factor disappears. Compose the softmax with almost any other loss and it does not. ## Reading the result For a three-class example with logits `[2.0, 1.0, 0.1]` and true class 0, the probabilities are about `[0.659, 0.242, 0.099]`, the loss is about `0.417`, and the gradient is `[-0.341, 0.242, 0.099]`. Three things to notice: 1. **The components sum to zero.** `sum_k (p_k - y_k) = 1 - 1 = 0`. This is the gradient counterpart of the fact that adding a constant to every logit does not change the probabilities - the loss has a flat direction along the all-ones vector, and the gradient never points along it. 2. **Every component lies in `[-1, 1]`.** The correct class gets `p_c - 1`, which is negative (push that logit up); every wrong class gets `+p_m` (push it down). The loss itself is unbounded, but its gradient with respect to the logits is not. 3. **The magnitude scales with the error.** Confidently correct (`p_c` near 1): the gradient is near zero, so the example stops contributing. Confidently wrong (`p_c` near 0): the correct-class component approaches `-1` and the winning wrong class approaches `+1`, the strongest signal available. That monotone, never-saturating relationship between wrongness and gradient magnitude is why this loss trains well. ## Soft targets Nothing in the derivation required `y` to be one-hot beyond the step where a single `p_c` appeared. Redo it with a general target distribution `q` and loss `-sum_k q_k * log p_k`, and you get ``` dL/dz = p - q ``` for any target vector whose entries sum to one - an averaged annotator distribution, a distillation target, a soft assignment. The form is identical; the one-hot case is just `q = y`. ## The batch, and the sign convention For a batch of `N` examples the reported loss is normally the mean, so each example's `p - y` is divided by `N` before it flows backward. Training then subtracts a multiple of that gradient. It is worth keeping the sign straight in an interview: `p - y` is the derivative of the loss you **minimise**, so the update moves the correct logit **up** and the others **down**. ## The mistake to avoid When this gradient is implemented as a fused unit that takes logits and a target and returns `p - y`, the softmax derivative is already inside it. Multiplying the softmax Jacobian in again downstream double-counts and produces an update that is wrong by a factor of `p * (1 - p)` - a bug that does not crash, merely trains badly, which is why interviewers like the derivation. The second common error is quoting `p * (1 - p)` as the answer: that is the derivative of a scalar sigmoid, not the gradient of this composed loss with respect to its logits.

  • What does the gradient look like when the model is confidently wrong?
    It is as large as this loss can make it and no larger. If the correct class sits at probability 0.001, its component is about -0.999 and the class that stole the mass is near +1. Every component still lies within [-1, 1], so a badly wrong example pushes at full rate without producing an exploding update - the loss value blows up, the logit gradient does not.
  • Why do the components of p - y always sum to zero?
    Because both vectors sum to one: the probabilities by construction, the one-hot target by definition. Geometrically it says the loss is unchanged when the same constant is added to every logit, so the gradient never has a component along that direction. It is a cheap correctness check on any implementation of this backward pass.
  • Does the form change if the target is a distribution rather than a one-hot vector?
    No. Redo the derivation with a target distribution q and the loss -sum_k q_k log p_k, and the result is dL/dz = p - q. The one-hot case is the special instance where q has a single 1. That is why soft targets - an averaged annotator distribution, a teacher model's output - drop straight into the same backward pass.

saying these in an interview costs you the question

  • Quotes p * (1 - p), the scalar sigmoid derivative, as the answer
  • Multiplies the softmax Jacobian in again after using the fused gradient
  • Says non-target classes receive no gradient
  • Claims the logit gradient grows without bound as the loss does
  • Gets the sign backwards and pushes the correct logit down

context