Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°10

Cross-Entropy Loss

Easy AI EngineeringDeep LearningLoss Functions

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.

SOLVE IT YOURSELF

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.

YOUR TASK

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.

CPython · WebAssembly
Examples and constraints 5 examples
EXAMPLE 1
Input p=[0.0, 1.0, 0.0], target=1
Output -0.0
all probability on the right class → no surprise, no loss (negating log(1) = 0.0 prints as -0.0, which equals 0)
EXAMPLE 2
Input p=[0.25, 0.25, 0.25, 0.25], target=0
Output ≈1.3863
a uniform guess over K=4 → loss = log(4)
EXAMPLE 3
Input p=[0.999, 0.0005, 0.0005], target=1
Output ≈7.6009
confident and wrong → large loss
EXAMPLE 4
Input p=[0.6, 0.3, 0.1], target=[0, 1, 0]
Output ≈1.2040
a one-hot target gives the same loss as the index target=1: −log(0.3)
EXAMPLE 5
Input p=[1.0, 0.0], target=1
Output ≈27.631
edge case: the zero probability is clamped to eps = 1e-12, so the loss is −log(1e-12), large but finite
CONSTRAINTS
  • 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-12 before taking its log, so log(0) never happens.
  • Pyodide has no numpy — use the math stdlib and plain lists only.
Hints 4 ideas
HINTS — 4 IDEAS
  1. The whole loss for an integer target is one line: -log(p[target]).
  2. Protect the log with max(p[target], eps) so a zero probability gives a big finite number, not -inf.
  3. If the target is a list (one-hot), sum -tᵢ·log(pᵢ) over all classes — only the tᵢ=1 term survives.
  4. 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.

stepclass itᵢpᵢ → max(pᵢ, eps)logtᵢ·logrunning sum
1000.5 → 0.5−0.6931−0.00.0
2100.0 → 1e-12−27.631−0.00.0
3210.25 → 0.25−1.3863−1.3863−1.3863
4300.25 → 0.25−1.3863−0.0−1.3863
5all———negate the sum1.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) returns 27.631…, which is −log(1e-12). Unclamped, Python’s math.log(0.0) raises ValueError: math domain error, and JavaScript’s Math.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]) returns 0.6931…. Unclamped, Python raises and JavaScript returns NaN, because 0 * -Infinity is NaN.
  • A perfect prediction returns -0.0 in Python and -0 in TypeScript, because negating log(1) = 0.0 flips 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 a list, so the reference uses it as an index and raises TypeError. A negative index passes silently in Python (target = -1 reads the last class), while in TypeScript p[-1] is undefined and the loss is NaN.
  • Soft labels go through the list branch: cross_entropy([0.7, 0.2, 0.1], [0.9, 0.05, 0.05]) returns 0.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], with logsumexp(z) = m + log Σ exp(zᵢ − m) and m = max(z). For z = [1000.0, 0.0] and target 1 this gives exactly 1000.0, while softmax-then-log underflows exp(−1000) to 0.0 and reports the clamp’s 27.63. PyTorch’s CrossEntropyLoss takes 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.6
Reference 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.6

Questions 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.

Finished this one? 0 / 76 Challenges done
NextLayer Normalization Next on the “Model internals” ladder · rung 3 of 6

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Challenges