Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Cross-Entropy Loss
The loss that trains almost every classifier and language model. It measures how surprised the model was by the right answer: if it put high probability on the true class, the loss is near 0; if it was confidently wrong, the loss explodes. No numpy — just a logarithm.
The problem
Implement cross-entropy loss for a single example. You are given a predicted probability distribution p (a list that sums to ~1) and a target. The target is either an integer class index or a one-hot list. Return the negative log-probability the model assigned to the true class: -log(p[target]) for an index, or -Σ tᵢ·log(pᵢ) for a one-hot target. Guard the logarithm with a tiny eps so a probability of 0 gives a large finite loss instead of -inf.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement cross_entropy(p, target, eps=1e-12). Return the negative log-probability of the true class — accepting either an integer index or a one-hot list as the target.
Examples and constraints 5 examples
p=[0.0, 1.0, 0.0], target=1-0.0p=[0.25, 0.25, 0.25, 0.25], target=0≈1.3863p=[0.999, 0.0005, 0.0005], target=1≈7.6009p=[0.6, 0.3, 0.1], target=[0, 1, 0]≈1.2040p=[1.0, 0.0], target=1≈27.631- For an integer
target, the loss is-log(p[target]). - For a one-hot list
target, the loss is-Σ tᵢ·log(pᵢ). - Clamp each probability to at least
eps = 1e-12before taking its log, solog(0)never happens. - Pyodide has no numpy — use the
mathstdlib and plain lists only.
Hints 4 ideas
- The whole loss for an integer target is one line: -log(p[target]).
- Protect the log with max(p[target], eps) so a zero probability gives a big finite number, not -inf.
- If the target is a list (one-hot), sum -tᵢ·log(pᵢ) over all classes — only the tᵢ=1 term survives.
- Perfect confidence (p on the true class = 1) gives log(1) = 0; a uniform guess over K classes gives log(K).
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious approach turns an integer target into a one-hot vector and always evaluates the full formula, −Σ tᵢ·log(pᵢ), over all K classes. It is correct in exact arithmetic, because every term with tᵢ = 0 is zero. It costs O(K) time and O(K) space for the one-hot vector, for an answer that depends on one number. Unguarded, it is also wrong on valid inputs: any class with probability 0 makes it take log(0), even a class whose weight is 0. Python’s math.log(0.0) raises ValueError, and JavaScript computes 0 * Math.log(0) as NaN.
The approach
With a one-hot target, only the true class survives the sum, so the loss is a single term: −log(p[target]). The reference branches on the target’s type. For an integer it returns -math.log(max(p[target], eps)), which is O(1). For a list it keeps the general sum, -sum(t * math.log(max(pi, eps)) for t, pi in zip(target, p)), which also accepts soft labels such as label-smoothed targets.
In both branches max(..., eps) clamps each probability to at least 1e-12 before the log, so a zero probability yields a large finite loss of about 27.63 instead of an error, and the clamp is applied term by term, so a zero in a weight-0 class is harmless too. Since every probability is at most 1, the loss is never negative, and it is 0 only when the true class gets probability 1.
Walk through an example
With p = [0.5, 0.0, 0.25, 0.25] and the one-hot target = [0, 0, 1, 0], the clamp keeps the zero-probability class 1 from crashing the sum, and only class 2 contributes, giving log 4 ≈ 1.3863.
| step | class i | tᵢ | pᵢ → max(pᵢ, eps) | log | tᵢ·log | running sum |
|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 0.5 → 0.5 | −0.6931 | −0.0 | 0.0 |
| 2 | 1 | 0 | 0.0 → 1e-12 | −27.631 | −0.0 | 0.0 |
| 3 | 2 | 1 | 0.25 → 0.25 | −1.3863 | −1.3863 | −1.3863 |
| 4 | 3 | 0 | 0.25 → 0.25 | −1.3863 | −0.0 | −1.3863 |
| 5 | all | — | — | — | negate the sum | 1.3863 |
Complexity
The integer form is O(1) time and space: one lookup and one log. The list form is O(K) time for K classes and O(1) extra space, since the sum runs over a generator. Building a one-hot vector for every integer target costs O(K) time and O(K) space per example, so a batch of n examples costs O(n·K) instead of O(n).
Edge cases to test
- Zero probability on the true class:
cross_entropy([1.0, 0.0], 1)returns27.631…, which is−log(1e-12). Unclamped, Python’smath.log(0.0)raisesValueError: math domain error, and JavaScript’sMath.log(0)returns-Infinity. - Zero probability on a weight-0 class in the list form:
cross_entropy([0.0, 0.5, 0.5], [0, 1, 0])returns0.6931…. Unclamped, Python raises and JavaScript returnsNaN, because0 * -InfinityisNaN. - A perfect prediction returns
-0.0in Python and-0in TypeScript, because negatinglog(1) = 0.0flips the sign bit. It compares equal to 0; only the printed sign differs. - The target’s type picks the formula: a tuple such as
(0, 1)is not alist, so the reference uses it as an index and raisesTypeError. A negative index passes silently in Python (target = -1reads the last class), while in TypeScriptp[-1]isundefinedand the loss isNaN. - Soft labels go through the list branch:
cross_entropy([0.7, 0.2, 0.1], [0.9, 0.05, 0.05])returns0.5166….
Common mistakes
- Summing
−log(p)over the whole distribution instead of taking only the target class. - Dropping the negative sign — loss must be non-negative and grow as the model gets it wrong.
- Taking
log(0)when the target probability underflowed to exactly 0 — clip with an epsilon.
Follow-ups an interviewer asks next
- The model outputs logits, not probabilities. How do you compute the loss stably?
- Skip the probabilities:
loss = logsumexp(z) − z[target], withlogsumexp(z) = m + log Σ exp(zᵢ − m)andm = max(z). Forz = [1000.0, 0.0]and target 1 this gives exactly 1000.0, while softmax-then-log underflowsexp(−1000)to 0.0 and reports the clamp’s 27.63. PyTorch’sCrossEntropyLosstakes raw logits for this reason. - How do you compute it over a batch?
- Average the per-example losses. For the examples
([0.7,0.2,0.1], 0),([0.1,0.8,0.1], 1)and([0.3,0.3,0.4], 0)the losses are about 0.357, 0.223 and 1.204, and the mean is about 0.595. The mean keeps the gradient scale independent of batch size. Language models average over every real token and mask out padding positions. - What about binary classification with a single probability?
- Use binary cross-entropy:
−(y·log p + (1 − y)·log(1 − p))for a label y of 0 or 1. It is this function’s two-class case with the distribution[1 − p, p]. Clamp p into[eps, 1 − eps]so neither logarithm ever sees 0.
Where this shows up
Cross-entropy is the loss that trains almost every classifier and language model: for an LM, the target is the actual next token and the loss is −log of the probability the model assigned it. Because the penalty explodes as that probability approaches 0, a single confidently-wrong label dominates the average — which is why label noise is so damaging and why models learn hard to never be certain and wrong.
Reference solution
Reference solution — Python
import math
def cross_entropy(p, target, eps=1e-12):
if isinstance(target, list):
# one-hot target: -sum(t * log(p))
return -sum(t * math.log(max(pi, eps)) for t, pi in zip(target, p))
# integer class index
return -math.log(max(p[target], eps))
print(cross_entropy([0.0, 1.0, 0.0], 1)) # -> 0.0
print(cross_entropy([0.25, 0.25, 0.25, 0.25], 0)) # ~1.3863 (= log 4)
print(cross_entropy([0.999, 0.0005, 0.0005], 1)) # ~7.6Reference solution — TypeScript
function crossEntropy(p: number[], target: number | number[], eps = 1e-12): number {
if (Array.isArray(target)) {
// one-hot target: -sum(t * log(p))
return -target.reduce((acc, t, i) => acc + t * Math.log(Math.max(p[i], eps)), 0);
}
// integer class index
return -Math.log(Math.max(p[target], eps));
}
console.log(crossEntropy([0.0, 1.0, 0.0], 1)); // -> 0
console.log(crossEntropy([0.25, 0.25, 0.25, 0.25], 0)); // ~1.3863 (= log 4)
console.log(crossEntropy([0.999, 0.0005, 0.0005], 1)); // ~7.6Questions about Cross-Entropy Loss
Why is cross-entropy loss the negative log of the true-class probability?
Cross-entropy between a target distribution t and a prediction p is the sum of t times log p over all classes, negated. With a one-hot target every class except the true one has t equal to 0, so only one term survives: minus the log of the probability the model gave the correct class. Minimizing it is maximum likelihood, because driving down the negative log of the correct probability drives that probability up. The log also turns a product of probabilities over many examples or tokens into a sum, which is easier to optimize and does not underflow.
Why does cross-entropy punish confident wrong answers so hard?
Because minus log p grows without bound as p approaches 0. Giving the true class 0.5 costs about 0.69, giving it 0.1 costs about 2.30, and giving it 0.0005, as in the third example, costs about 7.60. A model that is confidently wrong pays far more than one that is unsure, so one badly mislabeled example can dominate a batch average. That steep penalty pushes a model to spread probability when it is uncertain, and it is also why noisy labels hurt so much and why techniques such as label smoothing exist.
Why add a small epsilon before taking the log?
A probability can be exactly 0, because a model output underflowed or because the input was written that way, and the log of 0 is undefined. Python math.log raises ValueError: math domain error on 0.0, and JavaScript Math.log returns minus infinity, which turns the loss into infinity or, multiplied by a zero weight, into NaN. Clamping each probability to at least 1e-12 caps the loss at about 27.63, large but finite, so the bad prediction still stands out. Production training code avoids the problem instead by computing the loss from logits with log-softmax.
What is the cross-entropy of a uniform guess?
For K classes with probability 1/K each, the loss is minus log of 1/K, which is log K, whichever class is correct. With four classes that is log 4, about 1.386, the second example. It is a useful baseline when reading a training curve: a classifier whose loss sits near log K has learned nothing beyond guessing. The same idea applies per token in a language model, where the exponential of the average loss is the perplexity, so a uniform guess over V tokens has perplexity exactly V.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- SoftmaxThe function at the end of every classifier and language model: turn raw scores (logits) into a probability distribution. Implement the numerically stable version so big logits do not overflow. Solve it in Python or TypeScript.Read →
- Single-Head AttentionThe operation at the heart of every Transformer: scaled dot-product attention. Given queries, keys, and values, let each query pull a weighted blend of the values — softmax(QKᵀ/√d)·V — with a numerically stable softmax and no numpy, just the math. Solve it in Python or TypeScript.Read →
- Layer NormalizationThe stabilizer wrapped around every Transformer sub-layer: re-center and re-scale a vector to mean 0 and variance 1, then let learned gamma and beta stretch and shift it back — y = gamma·(x−mean)/√(var+eps) + beta. Keeps deep nets trainable. Solve it in Python or TypeScript.Read →
- TF-IDFThe scoring that ran search for decades — and still seeds hybrid retrieval today. Reward a word frequent in one document but rare across the corpus, shrug off words that appear everywhere: tf·log(N/df). Solve it in Python or TypeScript.Read →
- BPE Merge StepOne step of how every tokenizer vocabulary is built: count adjacent symbol pairs across the corpus, pick the most frequent (ties break lexicographically), and merge it everywhere, left to right, without overlaps. Run it a few thousand times and you have byte-pair encoding. Solve it in Python or TypeScript.Read →
- Beam Search DecoderGreedy decoding takes the best next token and never looks back — straight into garden paths. Keep the k best partial sequences alive at every step, with a product score and a clean tie-break, and watch k=2 escape a trap k=1 falls into. Solve it in Python or TypeScript.Read →