Roadmap / Loss Functions

✓

Language-Model Loss with Masking & Label Smoothing

mediumlosseslanguage-modelingmasking

The problem

A language model predicts the next token at every position of every text in the batch. So instead of a table of logits it outputs a 3-D array:

  • logits of shape (B,T,V)(B, T, V): BB texts, TT positions per text, VV scores per position (one per token of the vocabulary);
  • targets of shape (B,T)(B, T): the right next token at every position.

The loss is the cross-entropy of the previous exercise, averaged over all positions, with two extras that real training loops use. This exercise reproduces exactly what PyTorch's torch.nn.CrossEntropyLoss does with them.

Extra 1: ignored positions

Some positions must not count. Padding tokens are added only so that every text in the batch has the same length TT; in fine-tuning, the prompt tokens are not something the model should learn to write. Their target is set to a special value, ignore_index (default -100), and:

  • they add nothing to the loss;
  • the mean is taken over the MM positions that are not ignored.

Example. One text (B=1B = 1), T=3T = 3 positions, V=3V = 3 tokens, targets = [[0, 2, -100]]: the last position is padding. The logits of the first two positions are [2, 1, 0] and [0, 0, 0], whose losses are 0.4080.408 and 1.0991.099 (computed in the previous exercise). So M=2M = 2, and the loss is (0.408+1.099)/2≈0.753(0.408 + 1.099) / 2 \approx 0.753, whatever the logits of the padding position are.

Extra 2: label smoothing

Label smoothing with a parameter ε\varepsilon (epsilon, between 0 and 1) makes the target less absolute: instead of "100% the right token", it becomes 1−ε1 - \varepsilon on the right token plus ε\varepsilon spread evenly over the whole vocabulary. The loss of one position becomes:

ℓ=(1−ε) (−log⁡qy)+εV∑v=1V(−log⁡qv)\ell = (1 - \varepsilon)\,(-\log q_y) + \frac{\varepsilon}{V} \sum_{v=1}^{V} (-\log q_v)

and the loss of the batch is the mean over the kept positions:

L=1M∑kept positionsℓ\mathcal{L} = \frac{1}{M} \sum_{\text{kept positions}} \ell

where:

  • qq is the softmax of the position's logits, and −log⁡qv-\log q_v is its log-softmax with the sign flipped;
  • yy is the right token of the position, so −log⁡qy-\log q_y is the usual cross-entropy;
  • the second term is ε\varepsilon times the average of −log⁡qv-\log q_v over all VV tokens;
  • MM is the number of positions that are not ignored.

With ε=0\varepsilon = 0 this is plain cross-entropy.

Example. Same batch, ε=0.1\varepsilon = 0.1.

  • Position 0: −log⁡q≈(0.408,1.408,2.408)-\log q \approx (0.408, 1.408, 2.408), right token 0. ℓ=0.9⋅0.408+0.1⋅0.408+1.408+2.4083≈0.367+0.141=0.508\ell = 0.9 \cdot 0.408 + 0.1 \cdot \tfrac{0.408 + 1.408 + 2.408}{3} \approx 0.367 + 0.141 = 0.508.
  • Position 1: all logits equal, so −log⁡qv=1.099-\log q_v = 1.099 for every token and ℓ=0.9⋅1.099+0.1⋅1.099=1.099\ell = 0.9 \cdot 1.099 + 0.1 \cdot 1.099 = 1.099.
  • Position 2 is ignored. Mean of the other two: (0.508+1.099)/2≈0.803(0.508 + 1.099) / 2 \approx 0.803.

What to implement

lm_cross_entropy(logits, targets, ignore_index=-100, label_smoothing=0.0): return the single number L\mathcal{L}.

Requirements

  • Stable for any logits: compute the log-softmax with log-sum-exp.
  • ignore_index can be any integer, including -100. Never use an ignored target as an index. With a vocabulary of 100 tokens or more, logp[..., -100] does not fail: it silently reads a real token, the 100th from the end. With a smaller vocabulary it raises IndexError. Remove the ignored positions first.
  • If every position is ignored, return 0.0 (there is nothing to learn from), not nan.
  • No Python loops over positions.

Hints

  1. Flatten first: logits to shape (B⋅T,V)(B \cdot T, V) and targets to (B⋅T,)(B \cdot T,), with reshape. Now it is a table again, as in the previous exercise.
  2. keep = targets != ignore_index is a boolean mask; logits[keep] and targets[keep] keep only the rows that count.
  3. The second term of ℓ\ell is label_smoothing times the mean of the log-softmax row, with the sign flipped.
solution.py
Python idle

Press Run (⌘/Ctrl + Enter) to test your code.