Skip to content
Glacius

Learn the math behind machine learning.

Machine learning

The cross-entropy derivative, one logit at a time

Derive softmax cross-entropy with respect to logits, check p − y with a three-class example, and see what changes with soft labels, weights, and batch means.

In this article
  1. A three-class prediction you can calculate by hand
  2. Derive the gradient by writing the loss in logits
  3. Why the derivative with respect to probability is different
  4. Read the derivative as a local change in loss
  5. Verify the calculation with stable Python
  6. Soft labels, class weights, and batch means
  7. Match the formula to the loss you actually call
Three classes share a probability axis from zero to one. Predicted probabilities are 0.2, 0.5, and 0.3; the one-hot target is 0, 0, and 1. Blue filled circles show predictions and amber hollow circles show targets. The signed gaps, prediction minus target, are 0.2, 0.5, and −0.7. These are loss derivatives with respect to logits for one unweighted example with natural logarithms, not changes to the probabilities.Three classes share a probability axis from zero to one. Predicted probabilities are 0.2, 0.5, and 0.3; the one-hot target is 0, 0, and 1. Blue filled circles show predictions and amber hollow circles show targets. The signed gaps, prediction minus target, are 0.2, 0.5, and −0.7. These are loss derivatives with respect to logits for one unweighted example with natural logarithms, not changes to the probabilities.
Prediction minus target gives each logit derivative

One illustrative example: logits z = (ln 2, ln 5, ln 3) produce p = (0.2, 0.5, 0.3), and the target is class 3. Filled markers show predictions; hollow markers show targets. Horizontal distances use one probability scale. The signed gaps are the logit derivatives for unweighted cross-entropy with natural logarithms.

Original calculation from the displayed logits and target; not measured classification results.

Suppose a classifier gives the correct class a probability of 0.30.3. Its cross-entropy loss is ln(0.3)1.204-\ln(0.3)\approx1.204. Differentiate that loss with respect to the probability and you get 1/0.33.333-1/0.3\approx-3.333. Differentiate it with respect to the logit that produced the probability and you get 0.31=0.70.3-1=-0.7.

Both derivatives are correct. They describe changes to different inputs. The familiar cross-entropy gradient pyp-y is with respect to logits, after composing the loss with softmax. For one unweighted example, natural logarithms, and a fixed target distribution that sums to one,

Lzj=pjyj,p=softmax(z).\frac{\partial L}{\partial z_j}=p_j-y_j, \qquad p=\operatorname{softmax}(z).

We will calculate those derivatives for three classes, work through why every logit matters, and check the result by perturbing the inputs in Python.

A three-class prediction you can calculate by hand

A logit is a real-valued score before softmax converts the scores into probabilities. Choose

z=(ln2,ln5,ln3),y=(0,0,1).z=(\ln2,\ln5,\ln3), \qquad y=(0,0,1).

The target is class 3. Using logarithms for our starting scores makes the exponentials easy to check:

pj=ezjkezk,p=(210,510,310).p_j=\frac{e^{z_j}}{\sum_k e^{z_k}}, \qquad p=\left(\frac2{10},\frac5{10},\frac3{10}\right).

The prediction favors class 2 even though the target is class 3. For fixed targets yy, categorical cross-entropy is

L=i=13yilnpi.L=-\sum_{i=1}^{3} y_i\ln p_i.

With our one-hot target, only the third term contributes: L=lnp31.203973L=-\ln p_3\approx1.203973. The cross-entropy lesson explains this loss as the cost of assigning probability to outcomes; here we are interested in its sensitivity to the model's scores.

Subtract the target from the prediction one entry at a time:

ClassProbability pjp_jTarget yjy_jLogit derivative pjyjp_j-y_j
10.200.2
20.500.5
30.31−0.7

The positive derivatives mean that increasing either wrong-class logit raises the loss locally. The negative derivative for class 3 means increasing that logit lowers it. Gradient descent subtracts the gradient, so a direct update to these scores would lower the first two and raise the third.

Derive the gradient by writing the loss in logits

The shortest route avoids a full softmax Jacobian. Take the logarithm of the probability:

lnpi=ziln(kezk).\ln p_i=z_i-\ln\left(\sum_k e^{z_k}\right).

Substituting into the loss gives two terms:

L=iyizi+(iyi)ln(kezk).L=-\sum_i y_i z_i +\left(\sum_i y_i\right)\ln\left(\sum_k e^{z_k}\right).

Because the targets sum to one, this becomes

L=ln(kezk)iyizi.L=\ln\left(\sum_k e^{z_k}\right)-\sum_i y_i z_i.

When we differentiate with respect to zjz_j, the log-sum-exp term contributes ezj/kezk=pje^{z_j}/\sum_k e^{z_k}=p_j. The target-weighted sum contributes yjy_j. Therefore

Lzj=ezjkezkyj=pjyj.\frac{\partial L}{\partial z_j} =\frac{e^{z_j}}{\sum_k e^{z_k}}-y_j =p_j-y_j.

This is the same calculation developed in Dive into Deep Learning's softmax regression chapter. The result uses the whole probability distribution: changing one logit changes the denominator shared by every class.

Why the derivative with respect to probability is different

Before passing through softmax, the loss has partial derivatives

Lpi=yipi.\frac{\partial L}{\partial p_i}=-\frac{y_i}{p_i}.

This treats the positive coordinates of pp as separate inputs to the loss expression. The softmax operation then accounts for how those coordinates move together. For our example, the incoming gradient with respect to probabilities is (0,0,10/3)(0,0,-10/3).

The softmax Jacobian contains a derivative for each output probability with respect to each input logit:

pizj=pi(δijpj).\frac{\partial p_i}{\partial z_j} =p_i(\delta_{ij}-p_j).

Here δij\delta_{ij} is one when i=ji=j and zero otherwise. Increasing a class's own logit increases its probability at rate pi(1pi)p_i(1-p_i); increasing another class's logit decreases it at rate pipj-p_i p_j.

Only p3p_3 contributes directly to our one-hot loss. Its derivatives with respect to all three logits are

(p3z1,p3z2,p3z3)=(0.06,0.15,0.21).\left(\frac{\partial p_3}{\partial z_1}, \frac{\partial p_3}{\partial z_2}, \frac{\partial p_3}{\partial z_3}\right) =(-0.06,-0.15,0.21).

Multiplying by L/p3=10/3\partial L/\partial p_3=-10/3 yields (0.2,0.5,0.7)(0.2,0.5,-0.7) again. The wrong classes have nonzero logit gradients because both affect p3p_3 through softmax's denominator.

For a general normalized target, the multivariable chain rule sums all those paths:

Lzj=i(yipi)pi(δijpj)=yj+pjiyi=pjyj.\begin{aligned} \frac{\partial L}{\partial z_j} &=\sum_i\left(-\frac{y_i}{p_i}\right)p_i(\delta_{ij}-p_j)\\ &=-y_j+p_j\sum_i y_i\\ &=p_j-y_j. \end{aligned}

Multiplying pyp-y by the softmax derivative a second time would differentiate through softmax twice. Once you have pyp-y, you already have the gradient at the logits.

Read the derivative as a local change in loss

Hold z1=ln2z_1=\ln2 and z2=ln5z_2=\ln5 fixed, and vary only the correct-class logit z3=tz_3=t. The loss becomes a function of one number:

L(t)=ln(7+et)t.L(t)=\ln(7+e^t)-t.

At t=ln3t=\ln3, its slope is 3/101=0.73/10-1=-0.7. Increasing this logit by a small amount hh changes the loss by approximately 0.7h-0.7h. For example, h=0.01h=0.01 predicts a change of 0.007-0.007; the exact change is about 0.006989-0.006989.

Cross-entropy loss in natural-log units plotted against the class-3 logit t, holding class-1 and class-2 logits fixed at ln 2 and ln 5. The blue curve is L(t) = ln(7 + exp(t)) − t. At t = ln 3, loss is approximately 1.204 and the amber dashed tangent has slope −0.7. Increasing only the correct-class logit lowers the loss. The tangent approximates the curve locally, not for an entire training run.Cross-entropy loss in natural-log units plotted against the class-3 logit t, holding class-1 and class-2 logits fixed at ln 2 and ln 5. The blue curve is L(t) = ln(7 + exp(t)) − t. At t = ln 3, loss is approximately 1.204 and the amber dashed tangent has slope −0.7. Increasing only the correct-class logit lowers the loss. The tangent approximates the curve locally, not for an entire training run.
The correct-class logit has slope −0.7 at this prediction

Hold z₁ = ln 2 and z₂ = ln 5 fixed, and vary z₃ = t. The solid curve is L(t) = ln(7 + eᵗ) − t. The point marks t = ln 3 and loss ≈ 1.204. The dashed tangent has slope −0.7; increasing t by 0.01 lowers loss by approximately 0.006989.

Original one-coordinate calculation using the same three-class example and natural-log loss.

The tangent describes the nearby curve. Farther away, the probability changes and so does the derivative. The other two logits also have their own positive slopes at the starting point; this plot isolates the third coordinate rather than showing a full training trajectory.

As another check, the three logit derivatives sum to zero. Adding the same constant to every logit leaves softmax unchanged, so it must leave the loss unchanged as well.

Verify the calculation with stable Python

This example uses only Python's standard library. Subtracting the largest logit keeps the exponentials at or below one. Computing the loss from the shifted logits avoids taking the logarithm of a probability that may have rounded to zero.

import math


def cross_entropy(logits, target):
    if not logits or len(logits) != len(target):
        raise ValueError("Use equal, nonempty vectors")
    if not all(math.isfinite(v) for v in [*logits, *target]):
        raise ValueError("Inputs must be finite")
    if any(v < 0 for v in target) or not math.isclose(
        math.fsum(target), 1.0, rel_tol=0, abs_tol=1e-12
    ):
        raise ValueError("Target must be a probability distribution")

    largest = max(logits)
    shifted = [v - largest for v in logits]
    exponentials = [math.exp(v) for v in shifted]
    total = math.fsum(exponentials)
    probabilities = [v / total for v in exponentials]
    loss = math.log(total) - math.fsum(
        y * z for y, z in zip(target, shifted)
    )
    gradient = [p - y for p, y in zip(probabilities, target)]
    return loss, probabilities, gradient


z = [math.log(2), math.log(5), math.log(3)]
y = [0.0, 0.0, 1.0]
loss, p, gradient = cross_entropy(z, y)

# Central differences perturb one logit while fixing the others.
h = 1e-5
numeric = []
for j in range(len(z)):
    plus, minus = z.copy(), z.copy()
    plus[j] += h
    minus[j] -= h
    numeric.append(
        (cross_entropy(plus, y)[0] - cross_entropy(minus, y)[0])
        / (2 * h)
    )

assert max(abs(a - b) for a, b in zip(gradient, numeric)) < 1e-8
print("probabilities:", [round(v, 6) for v in p])
print("analytic:", [round(v, 6) for v in gradient])
print("numeric: ", [round(v, 6) for v in numeric])
print(f"loss: {loss:.6f}")

# A direct step in logit space, not a network parameter update.
new_z = [v - 0.5 * g for v, g in zip(z, gradient)]
print(f"loss after step: {cross_entropy(new_z, y)[0]:.6f}")

Both gradient lines print [0.2, 0.5, -0.7]. The loss changes from 1.203973 to 0.850053 after the direct logit update. The finite-difference check estimates each derivative using only evaluations of the loss, independently of the pyp-y expression.

In a neural network, the logits depend on weights and earlier activations. Backpropagation carries this gradient through those operations to the parameters before the optimizer takes a step. Our backpropagation and gradient descent example follows that process through an affine layer. Its squared-error loss has a different starting derivative; the same chain-rule machinery carries it backward.

Soft labels, class weights, and batch means

Soft labels still give p minus y

Nothing in the derivation required a single entry of yy to equal one. It required iyi=1\sum_i y_i=1, with the targets held fixed while differentiating the logits. If our target becomes (0.1,0.2,0.7)(0.1,0.2,0.7), the gradient at the same prediction is (0.1,0.3,0.4)(0.1,0.3,-0.4).

For label smoothing, substitute the actual smoothed target into pyp-y. Do not keep subtracting the original one-hot vector. TensorFlow's softmax cross-entropy documentation likewise requires each label vector to form a valid probability distribution.

Class weights change the expression

If the per-example loss is instead

Lw=iwiyilnpi,L_w=-\sum_i w_i y_i\ln p_i,

the same differentiation gives

Lwzj=pjiwiyiwjyj.\frac{\partial L_w}{\partial z_j} =p_j\sum_i w_i y_i-w_jy_j.

For a one-hot target in class cc, this simplifies to wc(pjyj)w_c(p_j-y_j): the target class's weight scales the entire gradient. For soft targets, multiplying each entry of pyp-y by its own class weight generally gives the wrong answer.

For instance, use w=(1,2,3)w=(1,2,3) and soft target y=(0.1,0.2,0.7)y=(0.1,0.2,0.7). Then iwiyi=2.6\sum_i w_i y_i=2.6, and the weighted gradient at p=(0.2,0.5,0.3)p=(0.2,0.5,0.3) is (0.42,0.90,1.32)(0.42,0.90,-1.32). It still sums to zero.

A mean introduces a denominator

For an unweighted batch of BB examples with Lmean=B1nLnL_{\text{mean}}=B^{-1}\sum_n L_n, the derivative for row nn is (pnyn)/B(p_n-y_n)/B. A summed loss has no 1/B1/B factor. This assumes each row contributes one classification loss and none are ignored.

Framework reductions deserve a check when using weights. PyTorch's weighted mean with class-index targets divides by the sum of the non-ignored target weights. With probability targets, its mean divides by the number of loss elements. These denominators can produce different gradient scales even when the unreduced losses match. See the explicit formulas in PyTorch's CrossEntropyLoss reference.

Match the formula to the loss you actually call

PyTorch's CrossEntropyLoss and TensorFlow's softmax_cross_entropy_with_logits accept logits and perform the required normalization internally. Passing softmax probabilities as their logit input changes the function being optimized. TensorFlow documents this input requirement explicitly.

For binary classification with one sigmoid output, the analogous result is also pyp-y with respect to its logit. The probability derivative differs: for L=ylnp(1y)ln(1p)L=-y\ln p-(1-y)\ln(1-p), it is (py)/(p(1p))(p-y)/(p(1-p)). The sigmoid derivative supplies the cancelling factor p(1p)p(1-p). Independent sigmoid outputs for multilabel classification therefore need their own loss setup, rather than one softmax across all labels.

All calculations here use natural logarithms. A base-two loss and its derivatives are larger by 1/ln21/\ln2. When checking a mismatch in your own implementation, first identify the differentiated variable, the target distribution, and the reduction. The short expression pyp-y is useful precisely because those choices are explicit.

Learn with Glacius

Want to go deeper into the math?

Build your understanding with visual lessons and practice on the concepts from this article.

Check the reasoning

Sources & notes

  1. Dive into Deep Learning: Softmax Regression

    Defines softmax and cross-entropy and derives the gradient with respect to logits.

  2. PyTorch 2.11: CrossEntropyLoss

    Specifies logit inputs, class weights, target formats, label smoothing, and reduction denominators.

  3. TensorFlow: softmax_cross_entropy_with_logits

    Documents normalized label distributions and the requirement to pass logits rather than softmax outputs.

Our figures use illustrative mathematical examples unless a dataset is explicitly identified. You can share the original Glacius figures with attribution and a link to this article; linked third-party material retains its own terms.

Keep exploring

Lessons behind this article