Skip to content
Skip to lesson
Back to Topics
LearnPreparation & PrerequisitesSoftmax, Cross-Entropy & Optimization
📝EasyNLP Fundamentals

Softmax, Cross-Entropy & Optimization

Turn raw class scores into stable probabilities and a useful learning signal, then apply the same loss to next-token predictions.

27 min read
Learning path
Step 22 of 199 in the full curriculum

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

The previous lesson predicted one continuous scalar: extra decode latency in milliseconds. Half squared error could say "raise the prediction by four units." A review triage bot faces a different challenge: pick exactly one category among bug, docs, and security.

Suppose the incident note reads, "This docs page is stale," while the model emits raw scores bug=3.0, docs=1.0, security=0.0. It picks bug, missing the correct label. What training signal should backpropagation send back through the network?

A binary right-or-wrong check gives the same result for a narrow near-miss and a confident mistake. Assigning docs an 11% probability should cost significantly more than assigning it 49%, even if bug wins the top spot in both runs. We need a mathematical bridge that turns classification scores into normalized probability shares, converts missed confidence into an exact penalty, and sends back a gradient on those scores. Normalization makes probabilities sum to one; calibration requires comparing predicted confidence with observed outcomes.

A decision begins as three raw scores

Our final linear layer outputs one real number per possible class. These numbers are : unconstrained scores. A linear classification head computes each score as a dot product between the internal representation and a class weight vector, plus a bias if present. Softmax later converts these real-valued scores into probabilities.

LabelCurrent logitWhat the incident note says
bug3.0Model's current favorite, but incorrect
docs1.0Correct supervised target
security0.0Plausible alternative, incorrect here

A larger logit means the network prefers that label over competitors. The value 3.0 is a score with no direct percentage interpretation. Read the table as an uncalibrated ranking: bug leads docs by two score points, but we haven't assigned any percentage share to any option.

Logits can be negative, zero, or wildly positive without breaking mathematics. A finite categorical distribution over mutually exclusive, exhaustive choices must have every probability pip_i in [0,1][0, 1] and sum to 11.

Predict the three checks in inspect-raw-logits.py before running it. bug takes the highest score, the raw sum is 4.04.0, and the list fails the probability test. The scores rank labels; an information-theoretic loss needs their normalized distribution.

inspect-raw-logits.py
labels = ["bug", "docs", "security"]
logits = [3.0, 1.0, 0.0]

best_index = max(range(len(logits)), key=logits.__getitem__)
print("largest logit:", labels[best_index])
print("raw score sum:", sum(logits))
print("valid probability distribution:", all(0 <= z <= 1 for z in logits) and sum(logits) == 1)
Output
largest logit: bug
raw score sum: 4.0
valid probability distribution: False

The raw scores identify a winner, but they don't yet say how much probability the correct label received.

Softmax turns score gaps into probability mass

turns each unconstrained score into a positive weight, then scales the weights so they sum to one. It does that by exponentiating each score and normalizing the results.

First, exponentiate each logit. Since ez>0e^z > 0 for every real number z∈(−∞,+∞)z \in (-\infty, +\infty), this turns even a negative score into a positive weight. Exponentiation also magnifies differences: a score lead of +2.0+2.0 becomes a weight multiplier of e2.0≈7.389e^{2.0} \approx 7.389.

Second, it sums those positive weights into a single shared denominator, called the partition function:

∑j=1Kezj\sum_{j=1}^K e^{z_j}

Dividing each exponential weight by this sum forces the output shares to sum to 1.01.0.

For our three triage scores, walk through the arithmetic: e3.0≈20.086e^{3.0} \approx 20.086, e1.0≈2.718e^{1.0} \approx 2.718, and e0.0=1.000e^{0.0} = 1.000. The shared sum is 23.80423.804.

LabelLogit ziz_iUnnormalized weight ezie^{z_i}Probability share pip_i
bug3.020.090.844
docs1.02.720.114
security0.01.000.042
Total (before rounding)≈23.8041.000

The general equation applies this two-step transformation to any vector z∈RKz \in \mathbb{R}^K:

pi=ezi∑j=1Kezjp_i = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}

Here pip_i is the probability assigned to class ii, ziz_i is its logit, and KK is the number of competing classes. Notice the distribution: the model assigns 0.844 to the incorrect bug label, leaving only 0.114 for the correct docs label. That value is the handoff to the loss function.

A support-ticket classifier turns logits bug 3.0, docs 1.0, and security 0.0 into probabilities. Subtracting the peak 3.0 gives 0, minus 2, minus 3; exponentiation gives weights 1, 0.135, 0.050, which sum to 1.185. Dividing each weight by that sum gives 84.4%, 11.4%, and 4.2%. The shared max shift keeps the largest exponent at one and preserves the probabilities.
Exponentiation stretches score gaps into positive weights. Subtracting the peak logit 3.0 caps every exponential at 1.0 to prevent floating-point overflow, while normalizing by the shared sum 1.185 preserves the exact probability shares.

Before running stable-softmax.py, confirm the invariants for these small scores: the class ranking stays preserved, every entry is positive, and the values sum to 1.0. Mathematically, softmax is strictly positive for finite logits; floating-point implementations can underflow tiny shares to zero and introduce rounding error in the sum.

stable-softmax.py
import math

def stable_softmax(logits: list[float]) -> list[float]:
    peak = max(logits)
    weights = [math.exp(z - peak) for z in logits]
    total = sum(weights)
    return [weight / total for weight in weights]

labels = ["bug", "docs", "security"]
logits = [3.0, 1.0, 0.0]
probabilities = stable_softmax(logits)

for label, probability in zip(labels, probabilities):
    print(f"{label:8s} {probability:.3f}")
print("sum     ", round(sum(probabilities), 3))
Output
bug      0.844
docs     0.114
security 0.042
sum      1.0

Stable arithmetic prevents floating-point overflow

Our gentle logits [3.0, 1.0, 0.0] fit easily inside standard floating-point variables. What happens when a deep network produces raw scores like [1000.0, 998.0, 997.0]?

If exponentiation runs in float32, its largest finite result is approximately 3.4×10383.4 \times 10^{38}, so exponential overflow starts around a logit of 88.7288.72. For float16, the largest finite value is 6550465504, and the threshold is about 11.0911.09: even e12e^{12} cannot fit. bfloat16 has roughly float32's range, with fewer precision bits.[1] An operation may compute in a wider dtype than its input; the implementation's compute dtype determines the exponent range used for exponentiation.

Once an exponential becomes +inf, the denominator can become +inf. Dividing inf / inf yields NaN (Not a Number). If you backpropagate through a nonfinite loss and step the optimizer, the invalid values can spread into parameters. Python's math.exp, used below, computes with ordinary Python floats (normally binary64) and raises OverflowError for math.exp(1000.0) instead of returning infinity.

Softmax possesses a key algebraic property: shift invariance.[2] Adding or subtracting any arbitrary scalar constant cc from every logit leaves the resulting probabilities completely unchanged:

ezi−c∑j=1Kezj−c=ezie−ce−c∑j=1Kezj=ezi∑j=1Kezj\frac{e^{z_i - c}}{\sum_{j=1}^K e^{z_j - c}} = \frac{e^{z_i} e^{-c}}{e^{-c} \sum_{j=1}^K e^{z_j}} = \frac{e^{z_i}}{\sum_{j=1}^K e^{z_j}}

The common factor e−ce^{-c} factors out of the sum and cancels between numerator and denominator. This identity unlocks the classic numerical stabilization trick: choose c=m=max⁡j(zj)c = m = \max_j(z_j), the maximum logit across the vector.

For a nonempty vector of finite logits, subtracting mm makes the largest shifted value zero (zmax−m=0z_{\text{max}} - m = 0). Every other shifted logit is nonpositive (zj−m≤0z_j - m \le 0); tied maxima are also zero. Exponentiating these values gives:

  1. The largest exponential is e0=1.0e^0 = 1.0. Overflow is mathematically impossible.
  2. Every other exponential lies in (0,1](0, 1].
  3. The denominator sum is at least 1.01.0, preventing division by zero.

Very negative shifted logits (such as −1000-1000) can underflow to 0.0, approximating a tiny probability by zero. Keep the loss in log space to avoid taking the logarithm of that rounded zero. Max-subtraction stabilizes finite values; validate inputs for NaN or +inf separately. A masked candidate may have logit -inf if at least one candidate has a finite score; an all-masked vector has no valid distribution.

overflow-then-stable.py
import math

def stable_softmax(logits: list[float]) -> list[float]:
    peak = max(logits)
    weights = [math.exp(z - peak) for z in logits]
    total = sum(weights)
    return [weight / total for weight in weights]

large_logits = [1000.0, 998.0, 997.0]
try:
    _ = [math.exp(z) for z in large_logits]
    naive_ok = True
except OverflowError:
    naive_ok = False

stable = stable_softmax(large_logits)
print("naive overflowed:", not naive_ok)
print("stable:", [round(probability, 3) for probability in stable])
print("stable sum:", round(sum(stable), 3))
Output
naive overflowed: True
stable: [0.844, 0.114, 0.042]
stable sum: 1.0

For a NumPy array in float32 or float64, naive exponentiation of these scores produces inf, and dividing the infinite entries by their infinite sum produces NaN (normally with warnings). Max-subtraction avoids that overflow and preserves the mathematical distribution, subject to floating-point rounding.

Cross-entropy prices missed predictions through surprise

Softmax gave us a probability distribution where docs received pdocs≈0.114p_{\text{docs}} \approx 0.114. How do we score that prediction?

measures the quality of a predicted distribution against ground truth. It's grounded in Maximum Likelihood Estimation (MLE) and information theory.

In information theory, observing an event with probability pp yields an information content, or surprise, of:

I(p)=−log⁡(p)I(p) = -\log(p)

We use natural logarithms throughout, measuring surprise in nats (using base-two logarithms would measure surprise in bits). If an event is certain (p=1.0p = 1.0), observing it brings zero surprise: −ln⁡(1.0)=0-\ln(1.0) = 0. If an event is extremely rare (p→0p \to 0), observing it brings near-infinite surprise: −ln⁡(0.001)≈6.91-\ln(0.001) \approx 6.91 nats.

When training a supervised model, the ground truth for an input is a target distribution yy. For single-label classification, yy is a one-hot where the correct class index cc gets weight 1.01.0 and all competing classes get 0.00.0. For our triage note:

y=[0, 1, 0]y = [0,\ 1,\ 0]

Cross-entropy calculates the expected surprise across classes under the supplied target distribution:

LCE=−∑k=1Kyklog⁡(pk)L_{\text{CE}} = -\sum_{k=1}^K y_k \log(p_k)

Because yk=0y_k = 0 for all non-target classes and yc=1y_c = 1 for the target, every term except the correct class multiplies by zero. Cross-entropy collapses to the Negative Log-Likelihood (NLL) of the true target class:

L=−log⁡(pc)L = -\log(p_c)

For our incident note where c=docsc = \text{docs}:

L=−log⁡(0.114195…)≈2.170 natsL = -\log(0.114195\ldots) \approx 2.170\text{ nats}

If the network had assigned 0.844 to docs, the loss would be only −log⁡(0.844)≈0.170-\log(0.844) \approx 0.170 nats. If it had placed 0.999 on docs, the loss would plummet to 0.0010.001 nats.

The log-sum-exp formulation prevents underflow crashes

Computing p = softmax(z) and then calling math.log(p[c]) introduces a subtle failure mode. With logits = [1000.0, 0.0, -1.0] and target docs, the target's shifted logit is −1000-1000. Its exponential underflows to 0.0. Python's math.log(0.0) raises ValueError; NumPy and PyTorch logarithms return -inf, giving an infinite negative log-likelihood. The finite logit difference gives a finite mathematical loss; floating-point underflow caused the failure.

The loss can be expressed directly in terms of raw logits, bypassing intermediate probabilities entirely.[2] Expanding pcp_c:

L=−log⁡(ezc∑jezj)=log⁡(∑j=1Kezj)−zcL = -\log\left(\frac{e^{z_c}}{\sum_j e^{z_j}}\right) = \log\left(\sum_{j=1}^K e^{z_j}\right) - z_c

The first term is . We stabilize it using the same maximum shift m=max⁡j(zj)m = \max_j(z_j):

log⁡(∑j=1Kezj)=log⁡(em∑j=1Kezj−m)=m+log⁡(∑j=1Kezj−m)\log\left(\sum_{j=1}^K e^{z_j}\right) = \log\left(e^m \sum_{j=1}^K e^{z_j - m}\right) = m + \log\left(\sum_{j=1}^K e^{z_j - m}\right)

Substituting this back into the loss expression:

L=(m−zc)+log⁡(∑j=1Kezj−m)L = (m - z_c) + \log\left(\sum_{j=1}^K e^{z_j - m}\right)

Grouping (m−zc)(m - z_c) first prevents catastrophic cancellation. When zc=0.0z_c = 0.0 and m=1000.0m = 1000.0, the first term evaluates cleanly to 1000.01000.0, while the log-sum evaluates to log⁡(1.0+… )≈0.0\log(1.0 + \dots) \approx 0.0. The loss returns 1000.01000.0 without ever taking the logarithm of zero.

cross-entropy-from-logits.py
import math

def cross_entropy_from_logits(logits: list[float], target_index: int) -> float:
    peak = max(logits)
    log_total = math.log(sum(math.exp(z - peak) for z in logits))
    return (peak - logits[target_index]) + log_total

ordinary = [3.0, 1.0, 0.0]
shifted_high = [z + 1000.0 for z in ordinary]

print("ordinary docs loss:", round(cross_entropy_from_logits(ordinary, 1), 3))
print("large-offset loss: ", round(cross_entropy_from_logits(shifted_high, 1), 3))
print("underflowed target weight:", math.exp(-1000.0))
print("finite target loss:", cross_entropy_from_logits([1000.0, 0.0, -1.0], 1))
print("equal huge scores:", round(cross_entropy_from_logits([1e20, 1e20, 1e20], 1), 3))
assert math.isclose(cross_entropy_from_logits([1e20] * 3, 1), math.log(3))
Output
ordinary docs loss: 2.17
large-offset loss:  2.17
underflowed target weight: 0.0
finite target loss: 1000.0
equal huge scores: 1.099

Entropy is reference uncertainty and KL divergence is prediction error

Before moving on to gradients, keep three related quantities separate. They use the same logarithms, but answer different questions about uncertainty and prediction error.

H(P)H(P) measures the inherent uncertainty already present inside a reference probability distribution PP:

H(P)=−∑kP(k)log⁡P(k)H(P) = -\sum_k P(k) \log P(k)

Cross-entropy H(P,Q)H(P, Q) measures the total cost of representing distribution PP using model distribution QQ. DKL(P∥Q)D_{\text{KL}}(P \parallel Q) measures the extra penalty caused specifically by the mismatch between QQ and PP:

H(P,Q)=H(P)+DKL(P∥Q)H(P, Q) = H(P) + D_{\text{KL}}(P \parallel Q)

For our one-hot docs target, P=[0,1,0]P = [0, 1, 0] has zero entropy (H(P)=0H(P) = 0): this annotation assigns all target mass to one label. Every fraction of a nat in the 2.1702.170 loss is mismatch with that target (DKL=2.170D_{\text{KL}} = 2.170). The annotation records its selected label; ambiguity and label noise require separate evidence.

When targets are soft (such as in distillation or human annotations with disagreement, like P=[0.5,0.5,0]P = [0.5, 0.5, 0]), the reference distribution has inherent entropy H(P)=ln⁡(2)≈0.693H(P) = \ln(2) \approx 0.693 nats. Even an ideal model (Q=PQ = P) will report a loss of 0.6930.693 nats, with DKL=0D_{\text{KL}} = 0. Don't mistake inherent target uncertainty for model failure.

entropy-cross-entropy-kl.py
from math import exp, inf, log

def entropy(probabilities: list[float]) -> float:
    return sum(-p * log(p) for p in probabilities if p > 0)

def cross_entropy(target: list[float], predicted: list[float]) -> float:
    # Inputs must be normalized, nonnegative distributions over the same classes.
    pairs = list(zip(target, predicted, strict=True))
    if any(p > 0 and q == 0 for p, q in pairs):
        return inf
    return -sum(p * log(q) for p, q in pairs if p > 0)

weights = [exp(value) for value in [3.0, 1.0, 0.0]]
predicted = [weight / sum(weights) for weight in weights]
one_hot_target = [0.0, 1.0, 0.0]
uncertain_target = [0.5, 0.5, 0.0]

one_hot_loss = cross_entropy(one_hot_target, predicted)
matched_loss = cross_entropy(uncertain_target, uncertain_target)

print(f"one-hot target entropy: {entropy(one_hot_target):.3f}")
print(f"one-hot cross-entropy:  {one_hot_loss:.3f}")
print(f"one-hot KL mismatch:    {one_hot_loss - entropy(one_hot_target):.3f}")
print(f"soft target entropy:    {entropy(uncertain_target):.3f}")
print(f"matching cross-entropy: {matched_loss:.3f}")
print(f"matching KL mismatch:   {matched_loss - entropy(uncertain_target):.3f}")
Output
one-hot target entropy: 0.000
one-hot cross-entropy:  2.170
one-hot KL mismatch:    2.170
soft target entropy:    0.693
matching cross-entropy: 0.693
matching KL mismatch:   0.000

These functions assume valid probability distributions; zip(..., strict=True) prevents a mismatched class list from silently losing terms. We use the convention 0log⁡0=00\log 0 = 0. If P(k)>0P(k)>0 but Q(k)=0Q(k)=0, cross-entropy and KL divergence are infinite. For valid distributions, KL is nonnegative and the minimum cross-entropy is the target entropy. Soft targets may express disagreement, distillation, or deliberate smoothing, so that entropy isn't always irreducible noise in the real task.

The combined gradient reveals a clean error signal

Loss tells us how poorly the model performed. To update weights, backpropagation needs the : the partial derivative of the loss with respect to each logit, ∂L∂zk\frac{\partial L}{\partial z_k}. The open question is whether that derivative stays readable after softmax, or whether the probability Jacobian buries the error.

Softmax and cross-entropy simplify together. The logarithm and the softmax derivative cancel, leaving a logit gradient of predicted probability minus target.[2]

Recall the loss in terms of raw logits:

L=log⁡(∑j=1Kezj)−zcL = \log\left(\sum_{j=1}^K e^{z_j}\right) - z_c

Differentiate with respect to any logit zkz_k:

∂L∂zk=∂∂zklog⁡(∑j=1Kezj)−∂zc∂zk\frac{\partial L}{\partial z_k} = \frac{\partial}{\partial z_k} \log\left(\sum_{j=1}^K e^{z_j}\right) - \frac{\partial z_c}{\partial z_k}

By the chain rule, the derivative of log⁡(u)\log(u) is 1u∂u∂zk\frac{1}{u} \frac{\partial u}{\partial z_k}. With u=∑jezju = \sum_j e^{z_j}:

∂∂zklog⁡(∑j=1Kezj)=1∑j=1Kezj⋅∂∂zkezk=ezk∑j=1Kezj=pk\frac{\partial}{\partial z_k} \log\left(\sum_{j=1}^K e^{z_j}\right) = \frac{1}{\sum_{j=1}^K e^{z_j}} \cdot \frac{\partial}{\partial z_k} e^{z_k} = \frac{e^{z_k}}{\sum_{j=1}^K e^{z_j}} = p_k

The derivative of the log-sum-exp term is softmax itself.

Meanwhile, the second term ∂zc∂zk\frac{\partial z_c}{\partial z_k} equals 11 if k=ck = c (the target class) and 00 otherwise. That is exactly the target weight yky_k:

∂L∂zk=pk−yk\frac{\partial L}{\partial z_k} = p_k - y_k

Vectorized across all classes:

∇zL=p−y\nabla_z L = p - y

Predicted probability minus true target. Look at the numbers for our triage example:

LabelPredicted probability ppTarget weight yyLogit gradient p−yp - yGradient descent update −η(p−y)-\eta(p - y)
bug0.8440+0.844Lowers wrong favorite
docs0.1141-0.886Raises correct target
security0.0420+0.042Lowers minor competitor

The zero-sum conservation property

Sum all elements of the gradient vector:

∑k=1K∂L∂zk=∑k=1K(pk−yk)=∑k=1Kpk−∑k=1Kyk=1.0−1.0=0\sum_{k=1}^K \frac{\partial L}{\partial z_k} = \sum_{k=1}^K (p_k - y_k) = \sum_{k=1}^K p_k - \sum_{k=1}^K y_k = 1.0 - 1.0 = 0

The gradients sum to zero. The upward pull on the correct class (−0.886-0.886) is exactly balanced by the downward push on competing classes (+0.844+0.042=+0.886+0.844 + 0.042 = +0.886). Softmax distributes a fixed unit of probability mass; learning to favor one class inevitably pulls mass away from others.

For each class, the diagram compares predicted probability p with one-hot target y on a zero-to-one line. The gradients p minus y are plus 0.844 for bug, minus 0.886 for docs, and plus 0.042 for security. Gradient descent subtracts these values, lowering the two competing logits and raising the docs logit. The three gradients sum to zero.
Each segment joins target y to prediction p; the labeled difference is the gradient p minus y. A direct gradient-descent step subtracts η times the gradient, raising docs and lowering its competitors in an exact zero-sum balance.

In a complete neural network, backpropagation channels this p−yp - y residual back to shared weights via the chain rule. Testing on logits directly makes the local mechanic transparent.

one-logit-signal-step.py
import math

def probabilities_and_loss(logits: list[float], target_index: int) -> tuple[list[float], float]:
    peak = max(logits)
    shifted = [z - peak for z in logits]
    log_total = math.log(sum(math.exp(value) for value in shifted))
    log_probs = [value - log_total for value in shifted]
    probabilities = [math.exp(value) for value in log_probs]
    return probabilities, -log_probs[target_index]

labels = ["bug", "docs", "security"]
logits = [3.0, 1.0, 0.0]
target_index = labels.index("docs")
target = [1.0 if index == target_index else 0.0 for index in range(3)]

before, before_loss = probabilities_and_loss(logits, target_index)
gradient = [probability - label for probability, label in zip(before, target)]
after_logits = [z - 0.5 * slope for z, slope in zip(logits, gradient)]
after, after_loss = probabilities_and_loss(after_logits, target_index)

print("gradient:", [round(value, 3) for value in gradient])
print("docs probability:", round(before[1], 3), "->", round(after[1], 3))
print("loss:", round(before_loss, 3), "->", round(after_loss, 3))
Output
gradient: [0.844, -0.886, 0.042]
docs probability: 0.114 -> 0.23
loss: 2.17 -> 1.469

Confirm the mathematical slope against numerical finite differences: nudge each logit by ϵ=10−5\epsilon = 10^{-5} and check the symmetric difference quotient.

verify-cross-entropy-gradient.py
import math

def loss(logits: list[float], target_index: int) -> float:
    peak = max(logits)
    shifted = [z - peak for z in logits]
    return math.log(sum(math.exp(value) for value in shifted)) - shifted[target_index]

logits = [3.0, 1.0, 0.0]
target_index = 1
peak = max(logits)
shifted = [z - peak for z in logits]
total = sum(math.exp(value) for value in shifted)
probabilities = [math.exp(value) / total for value in shifted]
analytic = [
    probability - (1.0 if index == target_index else 0.0)
    for index, probability in enumerate(probabilities)
]

epsilon = 1e-5
numeric = []
for index in range(3):
    plus = logits.copy()
    minus = logits.copy()
    plus[index] += epsilon
    minus[index] -= epsilon
    slope = (loss(plus, target_index) - loss(minus, target_index)) / (2 * epsilon)
    numeric.append(slope)

print("analytic:", [round(value, 6) for value in analytic])
print("numeric: ", [round(value, 6) for value in numeric])
print("match:", all(math.isclose(left, right, abs_tol=1e-6) for left, right in zip(analytic, numeric)))
Output
analytic: [0.843795, -0.885805, 0.04201]
numeric:  [0.843795, -0.885805, 0.04201]
match: True

Why squared probability error can learn slowly from confident mistakes

Why not use squared error on predicted probabilities: Lsq=12∑k(pk−yk)2L_{\text{sq}} = \frac{1}{2} \sum_k (p_k - y_k)^2? This is a scaled Brier loss, a valid proper scoring rule for categorical forecasts.[3] It can train classifiers. Here we compare its optimization behavior with cross-entropy; the sum above isn't an average over classes.

Examine how gradients flow through the softmax Jacobian matrix:

∂pi∂zj=pi(δij−pj)\frac{\partial p_i}{\partial z_j} = p_i (\delta_{ij} - p_j)

When differentiating cross-entropy LCE=−∑iyilog⁡(pi)L_{\text{CE}} = -\sum_i y_i \log(p_i) via the multivariate chain rule:

∂LCE∂zj=∑i∂L∂pi∂pi∂zj=∑i(−yipi)⋅pi(δij−pj)=−∑iyi(δij−pj)=pj−yj\frac{\partial L_{\text{CE}}}{\partial z_j} = \sum_i \frac{\partial L}{\partial p_i} \frac{\partial p_i}{\partial z_j} = \sum_i \left(-\frac{y_i}{p_i}\right) \cdot p_i (\delta_{ij} - p_j) = -\sum_i y_i (\delta_{ij} - p_j) = p_j - y_j

The factor −yipi-\frac{y_i}{p_i} has pip_i in the denominator, which cancels the pip_i in the softmax Jacobian numerator.

Under squared probability error, ∂Lsq∂pi=(pi−yi)\frac{\partial L_{\text{sq}}}{\partial p_i} = (p_i - y_i). There is no pip_i denominator to cancel the softmax Jacobian:

∂Lsq∂zj=∑i(pi−yi)pi(δij−pj)\frac{\partial L_{\text{sq}}}{\partial z_j} = \sum_i (p_i - y_i) p_i (\delta_{ij} - p_j)

When the model is confidently wrong (approximately pbug=0.9993p_{\text{bug}} = 0.9993 and pdocs=0.0003p_{\text{docs}} = 0.0003), the softmax Jacobian entries are small: the target diagonal is about 0.00030.0003, and the wrong favorite's is about 0.00070.0007. The squared-error logit gradient can become tiny, slowing learning in a saturated region. In this fixture, the values remain nonzero.

With cross-entropy, the target logit gradient is approximately 0.0003−1.0=−0.99970.0003 - 1.0 = -0.9997. This local signal remains strong even when the target probability is tiny; gradients farther back still depend on the network's Jacobians.

confidently-wrong-gradients.py
import math

def softmax(logits: list[float]) -> list[float]:
    peak = max(logits)
    weights = [math.exp(z - peak) for z in logits]
    total = sum(weights)
    return [weight / total for weight in weights]

def squared_probability_loss(logits: list[float], target: list[float]) -> float:
    error = [probability - label for probability, label in zip(softmax(logits), target)]
    return 0.5 * sum(value * value for value in error)

logits = [8.0, 0.0, 0.0]
target = [0.0, 1.0, 0.0]
probabilities = softmax(logits)
ce_gradient = [probability - label for probability, label in zip(probabilities, target)]

epsilon = 1e-5
mse_gradient = []
for index in range(3):
    plus = logits.copy()
    minus = logits.copy()
    plus[index] += epsilon
    minus[index] -= epsilon
    slope = (
        squared_probability_loss(plus, target)
        - squared_probability_loss(minus, target)
    ) / (2 * epsilon)
    mse_gradient.append(slope)

print("probabilities:", [round(value, 4) for value in probabilities])
print("cross-entropy gradient:", [round(value, 4) for value in ce_gradient])
print("squared-probability gradient:", [round(value, 4) for value in mse_gradient])
Output
probabilities: [0.9993, 0.0003, 0.0003]
cross-entropy gradient: [0.9993, -0.9997, 0.0003]
squared-probability gradient: [0.001, -0.0007, -0.0003]

In this fixture, the target logit gradients differ by about three orders of magnitude: roughly −1.0-1.0 versus −0.0007-0.0007. That stronger gradient explains why cross-entropy can be easier to optimize when the model is confidently wrong. Squared probability error remains a valid classification loss with different gradient behavior.

PyTorch expects unnormalized logits directly

In PyTorch, nn.CrossEntropyLoss expects unnormalized logits, not probabilities.[4]

For integer class targets without smoothing, this loss is equivalent to stable log_softmax followed by negative log-likelihood (NLLLoss). It works on CPU and GPU. Kernel count and intermediate allocation depend on backend and execution mode.[5]

If you insert nn.Softmax() before nn.CrossEntropyLoss, PyTorch treats the resulting probabilities pp as logits. The mistaken loss is −log⁡(softmax⁡(p)c)=log⁡(∑jepj)−pc-\log(\operatorname{softmax}(p)_c)=\log(\sum_j e^{p_j})-p_c. Valid shapes won't catch this mistake: it changes the objective and the gradients.

pytorch-cross-entropy.py
import torch
from torch import nn

logits = torch.tensor([[3.0, 1.0, 0.0]], requires_grad=True)
target = torch.tensor([1])  # docs
loss_fn = nn.CrossEntropyLoss()

loss = loss_fn(logits, target)
loss.backward()
probabilities = torch.softmax(logits.detach(), dim=1)

print("probabilities:", [round(value, 3) for value in probabilities[0].tolist()])
print("loss:", round(loss.item(), 3))
print("gradient:", [round(value, 3) for value in logits.grad[0].tolist()])
Output
probabilities: [0.844, 0.114, 0.042]
loss: 2.17
gradient: [0.844, -0.886, 0.042]

Now reproduce the common bug: softmax first, then CrossEntropyLoss. The shapes match, so Python raises no warning. Compare the target gradients.

do-not-softmax-before-cross-entropy.py
import torch
from torch import nn

target = torch.tensor([1])  # docs
loss_fn = nn.CrossEntropyLoss()

correct_input = torch.tensor([[3.0, 1.0, 0.0]], requires_grad=True)
correct_loss = loss_fn(correct_input, target)
correct_loss.backward()

wrong_input = torch.tensor([[3.0, 1.0, 0.0]], requires_grad=True)
wrong_loss = loss_fn(torch.softmax(wrong_input, dim=1), target)
wrong_loss.backward()

print("raw logits loss:", round(correct_loss.item(), 3))
print("probabilities passed as logits:", round(wrong_loss.item(), 3))
print("correct docs gradient:", round(correct_input.grad[0, 1].item(), 3))
print("distorted docs gradient:", round(wrong_input.grad[0, 1].item(), 3))
Output
raw logits loss: 2.17
probabilities passed as logits: 1.387
correct docs gradient: -0.886
distorted docs gradient: -0.127

The corrective gradient for docs shrank from −0.886-0.886 to −0.127-0.127. Feed raw logits directly into nn.CrossEntropyLoss. You can compute softmax separately for training metrics, distillation targets, or inference probabilities; the mistake is passing those probabilities as the loss's input. For integer targets, use torch.long class indices. Soft probability targets use floating-point tensors matching the input shape; PyTorch doesn't validate that they are normalized distributions.[4]

Temperature scaling shapes inference entropy

Our classification examples train at temperature T=1.0T = 1.0. Other objectives, including distillation, can use a different temperature during training. Scaling fixed logits by a positive T>0T > 0 changes how concentrated the distribution is:

pi(T)=ezi/T∑j=1Kezj/Tp_i(T) = \frac{e^{z_i / T}}{\sum_{j=1}^K e^{z_j / T}}

Dividing logits by TT changes how sharply probabilities concentrate:

  • When T<1.0T < 1.0, logit differences expand. As T→0+T \to 0^+, probability concentrates on the highest-scoring class if the maximum is unique. Tied maxima instead share the limiting mass equally.
  • When T>1.0T > 1.0, logit differences shrink. Mass spreads more evenly across competitors. As T→∞T \to \infty, the distribution approaches uniform uncertainty (1/K1/K).

Positive temperature preserves the mathematical ranking. Rescaling fixed logits doesn't update the network's weights, although a calibration procedure can fit temperature on held-out data. Softmax itself is undefined at T=0T=0; a serving API may use zero as a convention for greedy decoding. Greedy selection and drawing a sample from a sharpened distribution are different operations.

In distillation, Hinton et al. soften teacher and student distributions with higher temperatures to expose relationships among non-target classes ("dark knowledge"). Their experiments use different temperatures for different setups, so evaluate the value for the specific distillation task.[6] Cross-entropy through softmax⁡(z/T)\operatorname{softmax}(z/T) has raw-logit gradient (p(T)−y)/T(p(T)-y)/T. Distillation commonly multiplies the soft-target objective by T2T^2 to compensate for its high-temperature gradient scaling. During serving, temperature changes sampling diversity; the decoding policy still decides how a token is selected.

Temperaturebug (z=3z=3)docs (z=1z=1)security (z=0z=0)Interpretation
0.50.9800.0180.002Sharpened distribution; wrong winner reinforced
1.00.8440.1140.042Original learned model distribution
2.00.6290.2310.140Flattened distribution; alternatives gain mass
temperature-scales-logits.py
import math

def softmax_at_temperature(logits: list[float], temperature: float) -> list[float]:
    if not math.isfinite(temperature) or temperature <= 0:
        raise ValueError("temperature must be finite and positive")
    if not logits or not all(math.isfinite(z) for z in logits):
        raise ValueError("logits must be nonempty and finite")
    peak = max(logits)
    weights = [math.exp((z - peak) / temperature) for z in logits]
    total = sum(weights)
    return [weight / total for weight in weights]

logits = [3.0, 1.0, 0.0]
for temperature in (0.5, 1.0, 2.0):
    probabilities = softmax_at_temperature(logits, temperature)
    print(f"T={temperature:.1f}", [round(probability, 3) for probability in probabilities])

equal_scores = softmax_at_temperature([1e308, 1e308, 1e308], 1e-308)
print("equal huge scores:", [round(p, 3) for p in equal_scores])
assert all(math.isclose(p, 1 / 3) for p in equal_scores)
Output
T=0.5 [0.98, 0.018, 0.002]
T=1.0 [0.844, 0.114, 0.042]
T=2.0 [0.629, 0.231, 0.14]
equal huge scores: [0.333, 0.333, 0.333]

Subtract the peak before dividing by temperature. Dividing 1e308 by 1e-308 first produces infinity, turning a perfectly valid three-way tie into inf - inf and NaN. The shifted calculation keeps each maximum at zero. Temperature only reshapes the logits you already have. It doesn't stop training from pushing the winning score toward infinity. What objective keeps the best gap finite?

Label smoothing curbs overconfident logits

Nothing in the temperature rescaling keeps that gap finite. Consider optimizing the logits for one example without any regularization. What distribution minimizes the loss?

With a hard target ydocs=1.0y_{\text{docs}} = 1.0, minimizing cross-entropy loss L=−log⁡(pdocs)L = -\log(p_{\text{docs}}) requires driving pdocs→1.0p_{\text{docs}} \to 1.0. In softmax, pdocs→1.0p_{\text{docs}} \to 1.0 only occurs when:

zdocs−zk→+∞for all k≠docsz_{\text{docs}} - z_k \to +\infty \quad \text{for all } k \ne \text{docs}

One-hot cross-entropy approaches zero as the target logit gap grows without bound. On separable training data, an unregularized model may keep increasing score gaps after its classification accuracy reaches 100%. Conflicting labels, weight decay, early stopping, and shared parameter constraints can keep weight norms bounded.

Label smoothing changes the objective by mixing the one-hot target with a distribution over classes.[7] With a uniform mixture, parameter α∈(0,1)\alpha \in (0, 1), and KK classes:

y~k=(1−α)yk+αK\tilde{y}_k = (1 - \alpha) y_k + \frac{\alpha}{K}

For our 3-class triage problem with target docs (c=1c=1), α=0.1\alpha = 0.1, and K=3K = 3:

  • Target class: y~docs=(1−0.1)+0.13≈0.9333\tilde{y}_{\text{docs}} = (1 - 0.1) + \frac{0.1}{3} \approx 0.9333
  • Competitor classes: y~bug=y~security=0.13≈0.0333\tilde{y}_{\text{bug}} = \tilde{y}_{\text{security}} = \frac{0.1}{3} \approx 0.0333

What logit gap does the model need now to achieve zero gradient? Instead of an infinite score difference, the target probability reaches equilibrium when:

pdocspcompetitor=1−0.1+0.1/30.1/3=28  ⟹  zdocs−zcompetitor=ln⁡(28)≈3.332\frac{p_{\text{docs}}}{p_{\text{competitor}}} = \frac{1-0.1+0.1/3}{0.1/3} = 28 \implies z_{\text{docs}} - z_{\text{competitor}} = \ln(28) \approx 3.332

For this example, unrestricted logits attain their optimum at the smoothed distribution, with a finite gap of about 3.333.33. If pdocs>0.9333…p_{\text{docs}}>0.9333\ldots, its logit gradient becomes positive, so a direct gradient-descent step lowers that score. The minimum loss equals the positive entropy of the smoothed target. The objective controls relative scores, while adding a common constant to every logit leaves it unchanged; parameter norms need separate controls.

Label smoothing can improve generalization and calibration, but its benefits depend on the task and smoothing amount. Müller et al. also found that smoothing a teacher's labels could hurt subsequent distillation.[8] Evaluate α=0.1\alpha=0.1 on the target task; its effect depends on the label noise and smoothing amount.

PyTorch provides built-in label smoothing via nn.CrossEntropyLoss(label_smoothing=0.1).

label-smoothing-cross-entropy.py
import torch
from torch import nn

logits = torch.tensor([[3.0, 1.0, 0.0]], requires_grad=True)
target = torch.tensor([1])  # docs
alpha = 0.1
num_classes = 3

loss_fn = nn.CrossEntropyLoss(label_smoothing=alpha)
loss = loss_fn(logits, target)
loss.backward()

probs = torch.softmax(logits.detach(), dim=1)[0]
smoothed_target = torch.tensor([
    alpha / num_classes,
    (1.0 - alpha) + (alpha / num_classes),
    alpha / num_classes,
])
manual_loss = -torch.sum(smoothed_target * torch.log_softmax(logits.detach(), dim=1)[0])
grad_manual = probs - smoothed_target

print("probabilities:   ", [round(x, 3) for x in probs.tolist()])
print("smoothed target: ", [round(x, 3) for x in smoothed_target.tolist()])
print("pytorch loss:    ", round(loss.item(), 3))
print("manual loss:     ", round(manual_loss.item(), 3))
print("logit gradient:  ", [round(x, 3) for x in logits.grad[0].tolist()])
print("manual gradient: ", [round(x, 3) for x in grad_manual.tolist()])
Output
probabilities:    [0.844, 0.114, 0.042]
smoothed target:  [0.033, 0.933, 0.033]
pytorch loss:     2.137
manual loss:      2.137
logit gradient:   [0.81, -0.819, 0.009]
manual gradient:  [0.81, -0.819, 0.009]

The loss and gradient agree to the displayed precision. The manual calculation uses log_softmax to stay in log space. The same p−yp-y gradient identity works for normalized soft targets; for a malformed target with sum aa, the gradient would be ap−ya p-y, which is another reason to validate probability targets.

Next-token prediction extends the loss across sequences

Our triage classifier evaluated one decision. Autoregressive language models evaluate the exact same categorical decision at every sequence position.

Given context x1,x2,…,xtx_1, x_2, \dots, x_t, a language model emits a hidden state ht∈Rdh_t \in \mathbb{R}^d. Its output projection produces vocabulary logits zt∈R∣V∣z_t \in \mathbb{R}^{|V|}. The projection's weights may be tied to the input embedding matrix, but tying isn't required. The observed next token xt+1x_{t+1} provides the target.

For S+1S+1 observed tokens, there are SS next-token prediction positions when we use each token after the first as a target. With all those positions supervised, their mean negative log-likelihood is:

Lseq=−1S∑t=1Slog⁡P(xt+1∣x1,…,xt)L_{\text{seq}} = -\frac{1}{S} \sum_{t=1}^S \log P(x_{t+1} \mid x_1, \dots, x_t)

Reuse our numbers with vocabulary ["bug", "docs", "today"] and observed sequence bug docs today. Teacher forcing supplies inputs [bug, docs] and aligned targets [docs, today]:

  • Position 1 sees bug and predicts docs (c=1c=1) from logits [3.0, 1.0, 0.0]. Target probability is 0.1140.114, producing L1=2.170L_1 = 2.170 nats.
  • Position 2 sees bug docs and predicts today (c=2c=2) from logits [0.5, 0.0, 2.0]. Target probability is 0.7360.736, producing L2=0.306L_2 = 0.306 nats.
  • Sequence mean loss: Lseq=2.170+0.3062=1.238L_{\text{seq}} = \frac{2.170 + 0.306}{2} = 1.238 nats.
The negative-log curve L equals minus ln of the probability on the correct token. In the worked sequence, docs has target probability 0.114 and loss 2.170 nats; today has probability 0.736 and loss 0.306 nats. Averaging these two supervised positions gives 1.238 nats, with docs accounting for 87.6 percent of the summed loss.
The negative log penalty escalates rapidly when probability on the target drops toward zero. The missed docs token contributes 2.170 of the 2.476 total loss (about 87.6 percent), while correct today contributes only 0.306. Averaging over sequence positions reports a mean loss of 1.238 nats.

Notice the proportion: position 1 accounts for 2.170/2.476≈87.6%2.170 / 2.476 \approx 87.6\% of the sequence loss.

Tensor contracts and vocabulary memory scaling

In PyTorch, a language model batch emits logits shaped [B, S, V] (batch size, sequence length, vocabulary size) and targets shaped [B, S]. PyTorch's multi-dimensional cross-entropy expects classes on dimension 1: [B, V, S]. Alternatively, reshape both into 2D tensors: [B * S, V] with targets [B * S].

Vocabulary size can make [B, S, V] expensive. For a worked memory budget, choose B=16B=16, S=4096S=4096, and V=128256V=128256: logits alone occupy 16×4096×128256×4=33,621,540,86416\times4096\times128256\times4=33{,}621{,}540{,}864 bytes in float32, about 33.62 GB (31.31 GiB). Activations, gradients, optimizer state, and other tensors add to training memory.

Chunking the output projection and loss can reduce the largest intermediate. Specialized implementations such as Cut Cross-Entropy combine the projection with tiled log-sum-exp, avoiding a full token-by-vocabulary logit matrix in global GPU memory.[9] Its paper also studies gradient filtering, so approximation settings and supported objectives matter. An ordinary call to CrossEntropyLoss receives logits you've already allocated; projection-level memory savings require a specialized implementation.

sequence-loss-shapes.py
import torch
from torch import nn

logits = torch.tensor([[[3.0, 1.0, 0.0], [0.5, 0.0, 2.0]]])
targets = torch.tensor([[1, 2]])  # docs, today
loss_fn = nn.CrossEntropyLoss()

class_axis_loss = loss_fn(logits.transpose(1, 2), targets)
flattened_loss = loss_fn(
    logits.reshape(-1, logits.size(-1)),
    targets.reshape(-1),
)

print("logits shape:", tuple(logits.shape))
print("targets shape:", tuple(targets.shape))
print("class-axis loss:", round(class_axis_loss.item(), 3))
print("flattened loss:", round(flattened_loss.item(), 3))
Output
logits shape: (1, 2, 3)
targets shape: (1, 2)
class-axis loss: 1.238
flattened loss: 1.238

The logits in this example are already aligned to next-token targets. Some model APIs accept unshifted labels and perform the shift internally. Check that contract to avoid training on the current token or shifting twice. Causal attention must also mask future input tokens; a loss mask only controls which positions contribute to the objective.

Padding and prompt positions often aren't supervised. For integer targets, ignore_index=-100 removes those positions from both the loss and its mean denominator.[4] Here we add an ignored third position with deliberately extreme scores. Predict whether it changes the loss or receives a gradient.

masked-position-loss.py
import torch
from torch.nn import functional as F

logits = torch.tensor([
    [[3.0, 1.0, 0.0], [0.5, 0.0, 2.0], [100.0, -100.0, 0.0]]
], requires_grad=True)
targets = torch.tensor([[1, 2, -100]], dtype=torch.long)
valid_count = (targets != -100).sum()
if valid_count.item() == 0:
    raise ValueError("skip an optimizer step with no supervised targets")

position_losses = F.cross_entropy(
    logits.transpose(1, 2), targets, ignore_index=-100, reduction="none"
)
loss = position_losses.sum() / valid_count
loss.backward()

print("supervised positions:", valid_count.item())
print("position losses:", [round(x, 3) for x in position_losses[0].tolist()])
print("masked mean:", round(loss.item(), 3))
print("ignored gradient:", logits.grad[0, 2].tolist())
assert torch.count_nonzero(logits.grad[0, 2]).item() == 0
Output
supervised positions: 2
position losses: [2.17, 0.306, 0.0]
masked mean: 1.238
ignored gradient: [0.0, 0.0, 0.0]

With no class weights, the default mean pools all non-ignored targets across the batch. Each supervised target contributes equally: a sequence with one loss of 2 and another with three losses of 0 produce a pooled mean of 2/4=0.52/4=0.5. Giving each sequence equal weight would yield 1. Choose the weighting your objective needs. An all-ignored batch has an empty denominator; PyTorch's mean loss is NaN, so guard it before updating.

ignore_index masks target positions, not a vocabulary class. Suppose a padding token is class 2. At a position whose target is docs (class 1), the padding logit still competes in the softmax denominator, even with ignore_index=2. Compare that loss with explicitly excluding the padding class:

ignored-position-versus-excluded-class.py
import torch
from torch.nn import functional as F

logits = torch.tensor([[0.0, 1.0, 10.0]], dtype=torch.float64, requires_grad=True)
target = torch.tensor([1])
loss = F.cross_entropy(logits, target, ignore_index=2)
loss.backward()

allowed_logits = logits.detach().clone()
allowed_logits[:, 2] = -torch.inf
restricted_loss = F.cross_entropy(allowed_logits, target)

print(f"ignore_index=2 loss: {loss.item():.3f}")
print(f"padding-class gradient: {logits.grad[0, 2].item():.6f}")
print(f"padding excluded from candidates: {restricted_loss.item():.3f}")
assert logits.grad[0, 2] > 0.99 and restricted_loss < 0.32
Output
ignore_index=2 loss: 9.000
padding-class gradient: 0.999831
padding excluded from candidates: 0.313

The padding class receives a positive gradient because this supervised prediction put almost all its mass there. Excluding it changes the candidate distribution and the objective. Use a vocabulary mask only when the task really forbids that candidate, keep the target available, and leave at least one finite candidate. This fixture uses a hard target without label smoothing; assigning positive target mass to an excluded class instead makes the mathematical loss infinite.[4]

Class weighting changes reduction too. With integer targets and no smoothing, weight[c] multiplies each target loss, and PyTorch's mean divides by the sum of those weights over non-ignored positions. Floating-point probability targets use a different denominator: the number of positions. Even a one-hot float target can therefore differ from its integer form under class weights. Check the loss contract before combining weighted chunk means.[4]

The same reduction can be implemented without PyTorch using standard library loops:

average-position-losses.py
import math

def per_position_cross_entropy(logits: list[list[float]], targets: list[int]) -> list[float]:
    losses = []
    for row, target_index in zip(logits, targets, strict=True):
        peak = max(row)
        log_total = math.log(sum(math.exp(z - peak) for z in row))
        losses.append((peak - row[target_index]) + log_total)
    return losses

logits = [[3.0, 1.0, 0.0], [0.5, 0.0, 2.0]]
targets = [1, 2]
losses = per_position_cross_entropy(logits, targets)

print("position losses:", [round(value, 3) for value in losses])
print("mean loss:", round(sum(losses) / len(losses), 3))
Output
position losses: [2.17, 0.306]
mean loss: 1.238

Diagnostic failure modes in production pipelines

A bad loss usually fails in a recognizable place. Match the symptom to the arithmetic before you change the model:

SymptomProbable causeDiagnostic check and remediation
Loss evaluates to NaN or OverflowError occursDirect logit exponentiation without max-subtractionSubtract m=max⁡(z)m = \max(z) before exponentiating; verify inputs don't already contain NaN
Loss outputs +inf on initial stepsCalling log(softmax(z)) when a target probability underflows to zeroUse log-sum-exp directly on logits: (m - z_c) + log(sum(exp(z - m)))
Plausible loss but slow or stalled convergenceManual softmax layer placed before nn.CrossEntropyLossPass unnormalized logits directly into nn.CrossEntropyLoss
Confidently wrong predictions don't correctLoss function switched to MSE on probabilitiesUse cross-entropy; verify confident errors still produce logit gradients near ±1
Extreme confidence despite poor held-out accuracyOverfitting, label problems, or miscalibrationCheck the data and held-out predictions; evaluate regularization, label smoothing, and fitted temperature on the same cases
Sampling generates garbled or nearly uniform textExcessive temperature, incorrect tokenization, or bad logitsInspect tokens and score distributions; evaluate temperature and top-p on representative prompts
Sequence loss hides severe per-token failuresMean reduction washes out individual token spikesLog unreduced per-token losses alongside sequence means
Sequence mean is NaN despite finite logitsEvery target in a batch is ignoredCount supervised targets and skip an empty batch

Start with the failing example's input values, dtypes, shapes, target alignment, and supervised count. Then compare its stable loss and logit gradients with a small fixture. A sampling change can't repair a mislabeled or misaligned training objective.

Try a second target

Experiment with the running example: switch the correct label to security (c=2c = 2) while keeping logits [3.0, 1.0, 0.0].

Before running the code, calculate the expected loss and gradient by hand:

  • Target probability: psecurity≈0.042p_{\text{security}} \approx 0.042.
  • Loss: −log⁡(0.0420)≈3.170-\log(0.0420) \approx 3.170 nats.
  • Gradient p−yp - y:
    • bug: 0.844−0=+0.8440.844 - 0 = +0.844 (pushed down)
    • docs: 0.114−0=+0.1140.114 - 0 = +0.114 (pushed down)
    • security: 0.042−1.0=−0.9580.042 - 1.0 = -0.958 (pulled up strongly) Notice the zero-sum balance: +0.844+0.114−0.958=0.000+0.844 + 0.114 - 0.958 = 0.000. Subtracting η(p−y)\eta(p - y) raises the security logit while suppressing both competitors.
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A softmax receives logits [1000, 998, 997]. Why does subtracting the maximum before exponentiating preserve the mathematical distribution while preventing exponential overflow?
2.For labels [bug, docs, security], probabilities are [0.844, 0.114, 0.042] and the target is docs. If logits are updated directly by subtracting a positive step times p-y, how do they move?
3.Why should a PyTorch classifier pass raw logits into nn.CrossEntropyLoss?
4.Two next-token positions assign target probabilities 0.114 for docs and 0.736 for today. With mean cross-entropy, what is reported and which position matters more?
5.A decoder emits logits [B,S,V] and integer targets [B,S], with one vocabulary distribution per position. Which layouts satisfy PyTorch's class-dimension contract?
6.With logits [3,1,0], the correct label is docs, and temperature changes from 1.0 to 0.5. What happens to the probability distribution?
7.A model exactly matches the soft target [0.5,0.5,0]. What are its cross-entropy and KL divergence, using natural logarithms?
8.A target probability underflows when logits have very large gaps. Which expression is numerically stable and preserves the intended loss?
9.A classifier outputs approximately [0.9993, 0.0003, 0.0003], but class two is correct. How do softmax cross-entropy and squared probability error differ here?
10.For one example with unrestricted logits, why does cross-entropy against a strictly positive smoothed target have an optimum at finite logit gaps?

10 questions remaining.

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.