Learning to Predict

By the end of this chapter you will know exactly what a language model is optimizing, how to read the number on a loss curve, why the gradient of cross-entropy is so simple, and how a pile of probabilities gets turned into text at inference time.

We now have a whole architecture: tokens go in, embeddings pick up position, a stack of attention and MLP blocks mixes them, and a vector comes out for every position. But a freshly built transformer is a random function. It does not know any language. So how does it learn, and what exactly is it learning?

The honest answer is almost embarrassingly simple. We give the model a chunk of text, hide the next token, and ask it to guess. Then we measure how bad the guess was and nudge every weight a tiny bit in the direction that would have made the guess better. Repeat a few hundred billion times.

Everything in this chapter is that sentence unpacked: what "guess" means (a probability distribution over the vocabulary), what "how bad" means (cross-entropy), what "nudge" means (the gradient), and what to do with the guesses once training is over (sampling).

The objective: predict the next token

Here is the problem. Supervised learning usually needs labels, and labels are expensive. Nobody has hand-labeled the internet. So where do the training targets for a language model come from?

The trick is that text labels itself. Take the sentence "the cat sat on the mat". If the model has read "the cat sat on the", then the correct next token is "mat", and we know that because it is right there in the data. Every position in every document is a free training example, with the answer already attached.

One sequence, L training examples

This is worth being precise about, because it is the reason language models are so data-efficient per document. A sequence of $L$ tokens $x_1, x_2, \dots, x_L$ does not give one training example. It gives $L$ of them, all at once, in a single forward pass. At position $t$ the model sees $x_1 \dots x_t$ and must predict $x_{t+1}$.

Mechanically, we build the inputs and the targets from the same array by shifting it one step. If the data is the token id array [464, 3797, 3332, 319, 262, 2603], the input is everything but the last token and the target is everything but the first.

input x the cat sat on the (mat) target y cat sat on the mat x = tokens[:-1] y = tokens[1:] 5 tokens in → 5 predictions out → 5 losses, from ONE forward pass
Figure 1. The shift-by-one trick. The target at each position is simply the input one step to the right. Because attention is causal, position 3 ("sat") can only see "the cat sat", so its prediction of "on" is an honest guess, not a copy.

The causal mask from the attention chapter is what makes this legal. Without it, the hidden state at position $t$ could just look ahead and read $x_{t+1}$, and the loss would collapse to zero without the model learning anything.

Common confusion

People sometimes imagine training as "feed a prefix, predict one token, repeat". That would be $L$ separate forward passes. In reality one pass over an $L$-token sequence produces all $L$ predictions simultaneously, and all $L$ losses are averaged into one number. This parallelism is the whole reason transformers won over recurrent nets for pre-training.

Why such a dumb objective works

Predicting the next token sounds like autocomplete, and at small scale that is what it is: the model learns which words are frequent and which follow which. But to keep lowering the loss on real text, it has to keep learning more. To predict the next token of "the capital of France is", it has to know geography. To predict the next line of a Python function, it has to model what the function does. Compression of text, done well enough, requires modeling the world that produced the text. The objective is dumb; the data is not.

Where it came from

Training a neural network to predict the next word with a softmax over the vocabulary goes back to Bengio et al. (2003), "A Neural Probabilistic Language Model". The objective has not changed since; only the network in the middle and the amount of text have.

From hidden state to probabilities: the LM head

The last transformer block hands us a vector $h_t \in \mathbb{R}^{d}$ for each position. That is a point in a 768-dimensional space, not a guess about a token. How do we turn it into "I think the next token is 'mat' with probability 0.31"?

We need one score per vocabulary entry. So we multiply the hidden state by a matrix $W_U \in \mathbb{R}^{V \times d}$ that has one row per vocabulary item. This is the unembedding, also called the language-model head:

$$z_t = W_U\, h_t \qquad z_t \in \mathbb{R}^{V}$$

Each entry of $z_t$ is the dot product of $h_t$ with one vocabulary row, so it says "how much does this hidden state look like the direction for token $v$". These raw scores are called logits. They can be any real number, positive or negative, and they do not sum to anything in particular.

Then softmax turns the logits into a proper probability distribution: every entry positive, all entries summing to one, larger logits getting exponentially more mass.

$$p_t(v) = \frac{e^{z_t[v]}}{\sum_{u=1}^{V} e^{z_t[u]}}$$

That is the model's guess: a full distribution over what comes next. The forward pass of a language model ends here. Everything after this point is either measuring the guess (training) or acting on it (sampling).

Worked example: a 5-token vocabulary

Say the vocabulary is [the, a, cat, dog, zebra] and the LM head produces logits $z = [2.0,\ 1.0,\ 0.5,\ 0.1,\ -1.0]$.

Exponentiate: $e^{2.0}=7.389$, $e^{1.0}=2.718$, $e^{0.5}=1.649$, $e^{0.1}=1.105$, $e^{-1.0}=0.368$. Sum $=13.229$.

Divide: $p = [0.559,\ 0.205,\ 0.125,\ 0.084,\ 0.028]$. Check: they sum to $1.001$ (rounding). The model thinks "the" is most likely, and "zebra" is a long shot.

hidden stateh_t ∈ ℝ^768 W_U logitsz_t ∈ ℝ^50257 softmax probabilitiesp_t, sums to 1 lossor sample
Figure 2. The LM head. A 768-dimensional vector becomes 50,257 scores, then a distribution. In GPT-2 the unembedding matrix $W_U$ is the same matrix as the token embedding table (weight tying), which we will build in GPT-2 from Scratch.

Cross-entropy: loss as surprise

Now we have a distribution and we know the true next token. How bad was the guess? We want a single number that is small when the model put lots of probability on the right answer and large when it did not.

The number we use is the negative log of the probability the model gave to the correct token:

$$\mathcal{L}_t = -\log p_t(x_{t+1})$$

Read it as surprise. If the model said the true token had probability 1, the loss is $-\log 1 = 0$: no surprise. If it said probability 0.5, the loss is $\log 2 \approx 0.69$. If it said 0.01, the loss is $\log 100 \approx 4.6$. Probability near zero means surprise near infinity, which is exactly the punishment we want for confidently ruling out the truth.

Averaged over all positions in a sequence (and all sequences in a batch), this is the training loss:

$$\mathcal{L} = -\frac{1}{L}\sum_{t=1}^{L} \log p_t(x_{t+1})$$

The name "cross-entropy" comes from information theory. If $q$ is the true distribution of next tokens and $p$ is the model's, the cross-entropy is $-\sum_v q(v)\log p(v)$. Our data gives us only one sample from $q$ at each position (the token that actually appeared), so $q$ is a one-hot vector, and the sum collapses to the single term $-\log p(\text{correct})$. Same formula, seen from two angles.

Worked example, continued

Take the distribution from before, $p = [0.559, 0.205, 0.125, 0.084, 0.028]$ over [the, a, cat, dog, zebra].

If the true next token is "the": $\mathcal{L} = -\ln 0.559 = 0.58$ nats. Decent guess, small loss.

If the true next token is "cat": $\mathcal{L} = -\ln 0.125 = 2.08$ nats. The model was fairly wrong.

If the true next token is "zebra": $\mathcal{L} = -\ln 0.028 = 3.58$ nats. Big surprise, big loss.

Notice that the loss only looks at the probability of the one correct token. The model gets no direct credit for its second choice being reasonable.

What the loss looks like at initialization

Here is a useful sanity check that you will use every time you train a model. Before any training, the weights are random and small, so all the logits are close to zero, so softmax gives every token roughly the same probability: $p(v) \approx 1/V$. The loss is then

$$\mathcal{L}_{\text{init}} \approx -\log\frac{1}{V} = \log V$$

For GPT-2's vocabulary of $V = 50{,}257$ that is $\ln 50257 \approx 10.82$. For our 5-token toy vocabulary it is $\ln 5 \approx 1.61$. If your model reports a loss of 10.8 at step 0, the plumbing is right. If it reports 25, something (usually the initialization scale) is off, because the model is starting out confidently wrong, which is worse than clueless.

What does "loss = 3.0 nats" mean?

Loss is measured in nats when we use the natural log, or in bits when we use $\log_2$ (one nat is about 1.44 bits). A loss of 3.0 nats means that, on average, the correct token was assigned probability $e^{-3.0} \approx 0.05$. Not that every token got 5%; rather, the geometric mean of the probabilities assigned to the correct tokens was 5%. Some tokens (the "the" after "of") get 90%; some (the first word of a new sentence) get 0.1%; the average of the logs lands at 3.0.

For reference, GPT-2 small reaches roughly 3.3 nats per token on web text, and modern frontier models are reported around 2 nats or below on similar data, though the numbers depend heavily on the tokenizer and dataset, so never compare losses across different tokenizers.

Perplexity: the loss, exponentiated

Cross-entropy in nats is a bit abstract. Perplexity is the same number made intuitive: it is $e$ raised to the loss.

$$\text{PPL} = e^{\mathcal{L}} \qquad\text{equivalently}\qquad \mathcal{L} = \ln \text{PPL}$$

The interpretation: a perplexity of $N$ means the model is, on average, as uncertain as if it were choosing uniformly among $N$ tokens. Loss 3.0 nats is perplexity $e^{3} \approx 20$: the model is "effectively choosing among 20 tokens" at each step. At initialization, perplexity is $e^{\ln V} = V$: choosing uniformly among the whole vocabulary, which is exactly what it is doing.

Intuition

Perplexity is the branching factor. A model with perplexity 20 behaves as if the text forked 20 ways at every token. A perfect model of a deterministic text would have perplexity 1. Nothing real gets close to 1, because language genuinely has choices in it: "the cat sat on the ___" really could be "mat" or "floor" or "sofa", and no amount of training removes that ambiguity.

Common confusion

A single token's loss can exceed $\ln V$, and its perplexity can exceed $V$. In the worked example the "zebra" loss of 3.58 nats is a perplexity of 36, on a 5-token vocabulary. That is not a bug: it means the model was confidently wrong, worse than a uniform guess. Only the loss at initialization, averaged, sits at $\ln V$.

The companion code/lumen/eval.py computes perplexity properly: it sums the per-token losses over a held-out text, divides by the number of predicted tokens, and exponentiates once at the end (never average the per-sequence perplexities; average the losses).

InteractiveCross-entropy playgrounddrag the logits, pick the true token

Six logits, one true token. Watch the probability of the true token, the loss, and the gradient on each logit ($p - \text{onehot}$). Try raising the true token's logit and watch every other gradient shrink with it.

The gradient signal

We have a loss. Training means computing how the loss changes when each weight changes, and moving the weights the other way. Backpropagation does this through the whole network, but the very first step, the gradient of the loss with respect to the logits, has a form so clean it is worth memorizing.

If $y$ is the one-hot vector for the correct token (a 1 at the correct index, 0 elsewhere), then the gradient of the cross-entropy loss with respect to the logit vector is simply:

$$\frac{\partial \mathcal{L}}{\partial z} = p - y$$

That is it. The gradient on each logit is the probability the model assigned to that token, minus 1 if it was the correct token. Gradient descent moves logits in the negative gradient direction, so: the correct token's logit gets pushed up by $1 - p(\text{correct})$, and every wrong token's logit gets pushed down by $p(\text{wrong})$, proportional to how much probability it was wrongly given.

Worked example

With $p = [0.559, 0.205, 0.125, 0.084, 0.028]$ and true token "cat" (index 2), $y = [0,0,1,0,0]$, so

$p - y = [0.559,\ 0.205,\ -0.875,\ 0.084,\ 0.028]$.

"cat" gets a strong push up ($-0.875$ gradient means the update raises it). "the" gets the biggest push down, because it was the most confidently wrong. "zebra" barely moves; the model already thought it was unlikely, so there is nothing to fix.

This is the mechanism in one sentence: the model is told, at every position, "more of what happened, less of what you expected instead, in proportion to how wrong you were". When the model is already right ($p(\text{correct}) \to 1$) the gradient goes to zero and it stops learning from that token. The loss self-regulates: easy tokens stop contributing and hard tokens dominate.

Why is the gradient so simple? (the derivation)

Write the loss as $\mathcal{L} = -z_c + \log\sum_u e^{z_u}$, where $c$ is the correct index. The first term is the correct logit; the second is the log of the softmax denominator (the "log-sum-exp").

Differentiate with respect to $z_v$: the first term gives $-1$ if $v = c$ and $0$ otherwise. The second gives $e^{z_v}/\sum_u e^{z_u} = p(v)$. Add them: $\partial\mathcal{L}/\partial z_v = p(v) - \mathbb{1}[v=c]$. The exponential and the log cancel each other's mess, which is exactly why cross-entropy and softmax are always used together.

From the logits, backprop continues into $W_U$, then the last block, and so on down to the embedding table. Every parameter gets a gradient. We never write those by hand: PyTorch's autograd does it when we call loss.backward(). But it helps to know that everything upstream is being told, indirectly, the same thing: make the correct token more likely.

The training loop end to end

Let us zoom out and see one complete step of training. The question is: what happens between "here is a batch of text" and "the weights are slightly better"?

Symbols
$B$ = batch size (sequences)
$L$ = sequence length
$V$ = vocabulary size
$\theta$ = all parameters
$\eta$ = learning rate
STEP 1
Batch
Draw $B$ chunks of $L{+}1$ tokens; split each into input x = chunk[:-1] and target y = chunk[1:]. Shapes $(B, L)$.
STEP 2
Forward
Run the transformer: logits of shape $(B, L, V)$. Every position predicts its next token.
STEP 3
Loss
Cross-entropy at each of the $B \times L$ positions, averaged into one scalar $\mathcal{L}$.
STEP 4
Backward
Autograd computes $\nabla_\theta \mathcal{L}$ for every parameter, starting from $p - y$ at the logits.
STEP 5
Update
The optimizer moves $\theta \leftarrow \theta - \eta\cdot(\text{adjusted gradient})$, then zeroes the gradients. Go to Step 1.

In PyTorch the whole thing is a few lines. The companion code/lumen/train.py adds a learning-rate schedule, gradient clipping and mixed precision on top of this skeleton, but the skeleton is the important part:

import torch, torch.nn.functional as F

def train_step(model, optimizer, x, y):
    # x, y: (B, L) integer token ids; y is x shifted one to the right
    logits = model(x)                            # (B, L, V)
    loss = F.cross_entropy(                      # softmax + -log p(correct), averaged
        logits.view(-1, logits.size(-1)),        # (B*L, V)
        y.view(-1),                              # (B*L,)
    )
    optimizer.zero_grad(set_to_none=True)
    loss.backward()                              # gradients for every parameter
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    optimizer.step()
    return loss.item()
step 0 loss 10.83 (≈ ln 50257, model is uniform) step 100 loss 7.41 step 500 loss 5.62 step 2000 loss 4.38

Two details that matter. First, F.cross_entropy takes raw logits, not probabilities; it computes the softmax and the log together in a numerically stable way, so never apply softmax yourself before it. Second, the loss is averaged over all $B \times L$ positions, so a batch of 8 sequences of 1024 tokens is 8,192 next-token predictions per step.

batchx, y : (B, L) model(x)logits (B, L, V) cross-entropyscalar loss backward()∂loss/∂θ optimizer.step()θ ← θ − η · g updated weights feed the next forward pass forward: left to right. backward: right to left, same graph. One loop iteration = one "step" on the loss curve.
Figure 3. One training step. The forward pass produces logits at every position, the loss compresses $B \times L$ surprises into one number, backward fills in a gradient for every weight, and the optimizer applies them. Then the loop repeats with a fresh batch.

Reading a loss curve

The one plot every practitioner stares at is loss versus step. What does a healthy one look like, and how do you tell healthy from sick?

The fast drop

In the first few hundred steps the loss falls off a cliff, from $\ln V$ down to maybe 6 or 7 nats. This is the model learning unigram and bigram statistics: which tokens are common at all, and which tokens tend to follow which. That is cheap knowledge with a huge payoff, so gradient descent finds it first.

The long tail

After that the curve bends and flattens into a slow, grinding decline that never quite stops. Each additional nat of loss now requires something more expensive: syntax, then facts, then reasoning. On a log-log plot of loss against steps this tail is roughly a straight line, which is the empirical basis of scaling laws (Kaplan et al., 2020; you will meet them properly in Scaling Laws and Optimization).

Spikes and divergence

Sometimes the loss jumps up by a nat or two in a single step and then recovers over the next few hundred. That is a loss spike: a bad batch, a numerically unlucky attention logit, or a learning rate that is a touch too high. Occasional spikes that recover are tolerable. If the loss jumps and keeps climbing, or turns into a not-a-number, the run has diverged, and you lower the learning rate, tighten gradient clipping, or restore from a checkpoint before the spike.

What "flat" means

A flat loss curve does not mean the model has stopped learning. At loss 3.3 the model is still getting better at rare tokens, it is just that those improvements are invisible on a linear axis dominated by the easy tokens. Look at the curve on a log-x axis, or at held-out loss on a hard subset, before concluding a run has saturated.

InteractiveLoss-curve readerchange the learning rate, then scrub the step

A synthetic loss curve with the phases annotated. Higher learning rates drop faster at first, then start spiking, then diverge. The dashed line is $\ln V$ for $V=50257$.

Sampling: turning probabilities into text

Training is over. The model can now produce, for any prefix, a distribution over the next token. But a distribution is not text. To generate, we have to pick a token, append it to the prefix, and run the model again. The question is how to pick, and it turns out the obvious answers are both bad.

Greedy decoding: always take the best

The obvious choice is to take the highest-probability token every time, $\hat{x} = \arg\max_v p(v)$. This is deterministic and cheap, and for short factual answers it works fine. For anything longer it is boring and, worse, it gets stuck: the model wanders into a phrase, the most likely continuation of that phrase is more of the same phrase, and you get "I don't know. I don't know. I don't know." Holtzman et al. (2019) showed that greedy and beam-search text from GPT-2 is repetitive and has far higher per-token probability than real human text. Humans do not always say the most likely thing.

Pure sampling: roll the dice

The opposite choice is to sample from $p$ exactly. This fixes repetition and is what the model "really believes", but it is chaotic. The tail of the distribution is enormous: 50,000 tokens each at probability 0.0001 add up to a lot of probability, so every few tokens the sample lands on something weird, and once one weird token is in the context, the next distribution is conditioned on nonsense and things unravel. Everything below is a way to trim that tail while keeping some randomness.

Temperature: sharpen or flatten

Divide the logits by a temperature $T$ before the softmax:

$$p_T(v) = \frac{e^{z_v/T}}{\sum_u e^{z_u/T}}$$

With $T = 1$ nothing changes. With $T < 1$ the differences between logits get magnified, so the distribution sharpens toward the top choices; as $T \to 0$ it becomes greedy. With $T > 1$ it flattens toward uniform. A neat way to see it: $p_T(v) \propto p(v)^{1/T}$, so $T = 0.5$ squares the probabilities and renormalizes.

Top-k: keep only the k best

Zero out every token except the $k$ highest-probability ones, then renormalize the survivors (Fan et al., 2018). With $k = 50$ the model can still be creative among 50 plausible tokens but can never emit one of the 50,000 junk tokens. The weakness: $k$ is fixed, but the number of sensible continuations is not. After "the Eiffel Tower is in" there is one good answer; after "she opened the door and saw" there are hundreds. A fixed $k$ is too large for the first and too small for the second.

Top-p (nucleus): keep the smallest set that covers p

Sort tokens by probability and keep the smallest prefix whose cumulative probability reaches $p$, typically $0.9$ or $0.95$ (Holtzman et al., 2019). Then renormalize. When the model is confident, the nucleus is one or two tokens; when it is uncertain, the nucleus grows to hundreds. The cutoff adapts to the shape of the distribution, which is exactly what top-k could not do.

Repetition penalties

Even with good truncation, models like to repeat themselves. A repetition penalty (Keskar et al., 2019) divides the logit of any token that already appears in the context by a factor $\rho > 1$ (or multiplies it if the logit is negative), making reappearance less likely. A frequency penalty subtracts a constant times the count of previous occurrences; a presence penalty subtracts a constant if the token appeared at all. All of these are heuristics with no principled justification; they exist because they help.

Worked example: one distribution, five decoders

Suppose the next-token distribution over six candidates is $p = [0.40,\ 0.25,\ 0.15,\ 0.10,\ 0.06,\ 0.04]$.

Greedy: always token 1. Probability of picking token 1 is 100%.

Temperature 0.5: square and renormalize. $[0.16, 0.0625, 0.0225, 0.01, 0.0036, 0.0016]$, sum $0.260$, so $p_T = [0.615, 0.240, 0.087, 0.038, 0.014, 0.006]$. Sharper, but token 6 is still possible.

Top-k with k = 3: keep $[0.40, 0.25, 0.15]$, sum $0.80$, renormalize: $[0.50, 0.31, 0.19, 0, 0, 0]$.

Top-p with p = 0.6: cumulative sums are $0.40, 0.65, \dots$; the first prefix reaching $0.6$ has two tokens. Keep $[0.40, 0.25]$, renormalize: $[0.62, 0.38, 0, 0, 0, 0]$.

Pure sampling: $p$ unchanged. One time in 25 you get token 6.

InteractiveSampling playgroundadjust T, k, p, then sample

The model's distribution for the token after "The cat sat on the". Temperature is applied first, then top-k, then top-p (the order used by most libraries). Amber bars survive truncation; grey bars are zeroed. Press the button to draw 20 tokens from the final distribution.

The companion code/lumen/sampling.py implements all of these on a batch of logits, in the same order as above: temperature, then top-k, then top-p, then a torch.multinomial draw. The core is about ten lines:

def sample_next(logits, temperature=1.0, top_k=None, top_p=None):
    logits = logits / max(temperature, 1e-6)
    if top_k is not None:
        kth = torch.topk(logits, k=min(top_k, logits.size(-1))).values[..., -1, None]
        logits = logits.masked_fill(logits < kth, float("-inf"))
    if top_p is not None:
        sorted_logits, idx = torch.sort(logits, descending=True)
        cum = torch.softmax(sorted_logits, dim=-1).cumsum(-1)
        remove = cum - torch.softmax(sorted_logits, dim=-1) >= top_p   # keep the token that crosses p
        sorted_logits = sorted_logits.masked_fill(remove, float("-inf"))
        logits = torch.full_like(logits, float("-inf")).scatter(-1, idx, sorted_logits)
    probs = torch.softmax(logits, dim=-1)
    return torch.multinomial(probs, num_samples=1)

Beam search, and why it is rarely used for open-ended text

Beam search keeps the $b$ most probable partial sequences at each step instead of committing to one token, and at the end returns the whole sequence with the highest total log-probability. It was the workhorse of machine translation, where there is a short, nearly-correct answer to find. For open-ended generation it fails for the reason greedy fails, amplified: it finds sequences that are too probable. Highly probable text is generic, repetitive, and short, and beam search is very good at finding it. Stahlberg and Byrne (2019) even showed that for many translation models the single most probable output is the empty string. So in chat and creative settings, everyone samples.

Teacher forcing and exposure bias

One last subtlety about how training and generation differ. During training, at every position the model is conditioned on the true previous tokens from the data, regardless of what it would have predicted. This is called teacher forcing, and it is what makes the parallel, shift-by-one training possible. At inference, the model is conditioned on its own previous outputs. So a model that makes one mistake finds itself in a context it never saw during training, and can compound the error; this mismatch is called exposure bias (Ranzato et al., 2015). Remedies such as scheduled sampling (Bengio et al., 2015) exist, but in practice large models are trained with plain teacher forcing and the problem is mostly handled by the sheer breadth of contexts they have seen, plus the post-training methods of the next chapter, which do train on model-generated text.

Practice

Exercise 1 — cross-entropy by hand, then by torch

Take logits [1.5, 0.3, -0.2, 2.2] and correct index 0. Compute the softmax, the loss and the gradient $p - y$ with a calculator. Then verify with F.cross_entropy(torch.tensor([logits]), torch.tensor([0])) and logits.grad after backward(). They must agree to three decimals.

Solution sketch

$e^{1.5}=4.482$, $e^{0.3}=1.350$, $e^{-0.2}=0.819$, $e^{2.2}=9.025$; sum $15.676$. $p=[0.286, 0.086, 0.052, 0.576]$. Loss $=-\ln 0.286 = 1.252$. Gradient $=[0.286-1, 0.086, 0.052, 0.576] = [-0.714, 0.086, 0.052, 0.576]$. The correct token gets pushed up hardest; the wrong token with 58% gets pushed down hardest. Make the logits a tensor with requires_grad=True and compare.

Exercise 2 — measure perplexity

Use code/lumen/eval.py (or write your own 15-line version) to compute the perplexity of pretrained GPT-2 small on (a) a paragraph of Wikipedia, (b) the same paragraph with the words shuffled, (c) a paragraph of Python code. Predict the ordering before you run it.

Solution sketch

Tokenize, run the model with the input as its own target (shifted), average the per-token losses, exponentiate once. Expect roughly: Wikipedia lowest (perplexity in the tens), code higher, shuffled words far higher (hundreds or thousands), because word order is most of what the model has learned. Do not average perplexities across chunks; average losses, then exponentiate.

Exercise 3 — decoders side by side

With code/lumen/sampling.py and pretrained GPT-2, generate 100 tokens from the prompt "In a shocking finding, scientists" using greedy, pure sampling, $T=0.7$, top-k 40, and top-p 0.9. Count how many distinct tokens each produced and how many times the longest repeated 4-gram occurs.

Solution sketch

Greedy will loop within 30 to 60 tokens (low distinct count, high repeated 4-grams). Pure sampling has the most distinct tokens but drifts off-topic. Top-p 0.9 or top-k 40 with $T \approx 0.8$ is usually the sweet spot: coherent and non-repetitive. Write down the seed you used so the comparison is reproducible.

Check yourself
A sequence of 512 tokens is passed through the model once during training. How many next-token predictions are scored?
With the shift-by-one trick, every position predicts the token after it, so a 512-token chunk gives 511 (input, target) pairs, scored in one pass. The causal mask keeps each prediction honest.
Your untrained model has a 32,000-token vocabulary. What loss should you see on the first step?
Random small weights give a near-uniform distribution, so the correct token has probability about 1/V and the loss is −ln(1/V) = ln V. Much larger means the init is too confident; much smaller means information is leaking (check the mask).
The gradient of cross-entropy with respect to the logits is p − y. What does this say about a wrong token the model assigned probability 0.001?
The gradient on a wrong token equals its probability, 0.001 here, so its logit barely moves. The loss spends its effort on confidently wrong tokens and on raising the correct one.
A model reports a held-out loss of 2.3 nats per token. Its perplexity is roughly:
Perplexity is e raised to the loss in nats: e^2.3 ≈ 10. The model behaves as if choosing uniformly among about 10 tokens at each step.
Why does top-p (nucleus) sampling usually beat top-k on open-ended text?
Top-p keeps the smallest set of tokens whose probability mass reaches p: one token when the model is sure, hundreds when it is not. Top-k keeps a fixed count regardless, which is too many in confident spots and too few in open ones.

Key takeaways

  • The objective is next-token prediction; the targets are the input shifted by one, so one sequence of $L$ tokens is $L$ training examples in a single forward pass.
  • The LM head maps the hidden state to $V$ logits; softmax turns them into probabilities; the loss is $-\log p(\text{correct})$, the model's surprise.
  • At initialization the loss is $\approx \ln V$ (10.82 for GPT-2's vocabulary). Perplexity $= e^{\text{loss}}$ is the effective number of choices per token.
  • The gradient on the logits is $p - y$: raise the correct token, lower the wrong ones in proportion to how much probability they stole.
  • A healthy loss curve drops fast (token statistics), then decays slowly (everything else). Spikes that recover are fine; a climb that does not is divergence.
  • Greedy decoding is repetitive and pure sampling is chaotic; temperature, top-k and top-p trim the tail. Beam search finds text that is too probable, so it is rarely used for open-ended generation.

Further reading