Nathan Lambert
From cross-entropy to preferences and reinforcement learning — a short, optional refresher before Lecture 1.
This deck was drafted with assistance from GLM 5.2.
No prior reinforcement learning is assumed. We assume intro-ML comfort with losses, gradients, and probability, plus basic PyTorch tensor fluency. This deck is a refresher on autoregressive language models and the handful of ideas the course uses:
A language model learns the joint probability of a sequence of tokens (words / subwords) in an autoregressive manner — each prediction depends on the tokens before it.
Given x = (x_1, \ldots, x_T), the model factorizes the sequence probability:
Modern LMs (ChatGPT, Claude, Gemini) are decoder-only Transformers built on self-attention — each position attends only to itself and the positions before it.

The transformer backbone outputs a hidden state vector per token. The LM head is a final linear projection from the hidden dimension back to the vocabulary (tokenizer space).
The same backbone can carry different heads for different jobs. In this course the first example is a reward-model head (Chapter 5) — same transformer, new head, new objective.
Softmax turns the logits z^{(t)} at step t into the next-token distribution — producing each per-token probability:
A whole sequence is the product of these terms (chain rule), and in log-space the product becomes a sum:
So every term in the sum is a log_softmax of the logits read off at the true token — which is exactly the (negative) cross-entropy loss at that position.
Why log-probs? Numerical stability (products of many small probabilities underflow), and sums are easier to differentiate than products.
Lectures discuss shifting logits, gathering target log-probs, and masking. Here is the whole path once, at the level of tensor shapes:
input_ids ∈ ℕ^{B × L} # INPUT TO MODEL -- token IDs, batch B, length L
→ model(input_ids).logits ∈ ℝ^{B × L × V} # one vector per position, V = vocab
→ log_softmax + gather ∈ ℝ^{B × (L-1)} # log-prob of each observed token
→ (× completion_mask).sum ∈ ℝ^{B} # one completion log-prob per sequence
Next: the same path in PyTorch, one line at a time.
logits = model(input_ids).logits # (B, L, V): a score for every vocab token, at every position
logits = model(input_ids).logits # (B, L, V): a score for every vocab token, at every position
logits = logits[:, :-1, :] # drop the last position — its prediction has no next token to score
logits = model(input_ids).logits # (B, L, V): a score for every vocab token, at every position
logits = logits[:, :-1, :] # drop the last position — its prediction has no next token to score
labels = input_ids[:, 1:] # the target at each position is just the actual next token (the shift)
logits = model(input_ids).logits # (B, L, V): a score for every vocab token, at every position
logits = logits[:, :-1, :] # drop the last position — its prediction has no next token to score
labels = input_ids[:, 1:] # the target at each position is just the actual next token (the shift)
completion_mask = completion_mask[:, 1:] # shift the mask the same way so it lines up with labels (1 = completion)
logits = model(input_ids).logits # (B, L, V): a score for every vocab token, at every position
logits = logits[:, :-1, :] # drop the last position — its prediction has no next token to score
labels = input_ids[:, 1:] # the target at each position is just the actual next token (the shift)
completion_mask = completion_mask[:, 1:] # shift the mask the same way so it lines up with labels (1 = completion)
log_probs = logits.log_softmax(dim=-1) # normalize logits into log-probabilities over the vocab -> (B, L-1, V)
logits = model(input_ids).logits # (B, L, V): a score for every vocab token, at every position
logits = logits[:, :-1, :] # drop the last position — its prediction has no next token to score
labels = input_ids[:, 1:] # the target at each position is just the actual next token (the shift)
completion_mask = completion_mask[:, 1:] # shift the mask the same way so it lines up with labels (1 = completion)
log_probs = logits.log_softmax(dim=-1) # normalize logits into log-probabilities over the vocab -> (B, L-1, V)
token_log_probs = log_probs.gather(-1, labels.unsqueeze(-1)).squeeze(-1) # read off the log-prob of each TRUE next token -> (B, L-1)
logits = model(input_ids).logits # (B, L, V): a score for every vocab token, at every position
logits = logits[:, :-1, :] # drop the last position — its prediction has no next token to score
labels = input_ids[:, 1:] # the target at each position is just the actual next token (the shift)
completion_mask = completion_mask[:, 1:] # shift the mask the same way so it lines up with labels (1 = completion)
log_probs = logits.log_softmax(dim=-1) # normalize logits into log-probabilities over the vocab -> (B, L-1, V)
token_log_probs = log_probs.gather(-1, labels.unsqueeze(-1)).squeeze(-1) # read off the log-prob of each TRUE next token -> (B, L-1)
seq_log_probs = (token_log_probs * completion_mask).sum(dim=-1) # zero out prompt/pad, then sum per sequence -> (B,)
The one-token shift is the autoregressive step made literal: position t’s logits predict token t+1. Sum the per-token log-probs and loss.backward() — autodiff turns that sum into the corresponding sum of per-token gradients.
Masking is where silent bugs live in LLM code. Three different masks do three different jobs:
(B, L).1 only on completion tokens; the loss is multiplied by it before summing. Prompt tokens condition the prediction but contribute no loss.Get the completion mask wrong and you either train on the prompt (wastes gradient on tokens you can’t change) or on padding (trains on noise).
Generation samples/searches over \pi_\theta — same weights, many ways to pick each token:
Key split: the distribution \pi_\theta vs the procedure that samples it.
At each position, cross-entropy compares the model’s predicted distribution to the true next-token label q_t. That label is a one-hot at the actual token x_t, so the sum over the vocabulary collapses to a single term:
So the “comparison to the true token” is just the one-hot picking out one log-prob. Summing over positions and averaging over data gives the LM loss — equivalently, the negative log-likelihood (NLL):
“Predict the next token” is supervised learning — the label is just the next token in the sequence.
Training repeats one step:
The basic backprop loop is shared across pretraining, mid-training, and fine-tuning — what differs is masking, sampling, batching, data construction, and systems, not the optimizer.
The boundary at the end of pretraining is fuzzy and very important to post-training:
Modern post-training pipelines start from a mid-trained checkpoint, so this is where the course’s story effectively begins.
The Kullback–Leibler divergence measures how one distribution differs from another:
Three things to remember:
Related: Entropy H(p) = -\sum_i p_i \log p_i is the uncertainty of a distribution (uniform = high, peaked = low). It shows up in definitions, derivations, and understanding of these systems. Very similar computation.
You will see KL in essentially every alignment lecture.
Three quantities, one information-theoretic picture (measured in nats with \ln, bits with \log_2):
They are one identity:
So minimizing cross-entropy is minimizing KL to the truth — H(p) is fixed by the data. In LM training the label p is one-hot, so H(p)=0 and cross-entropy = KL = NLL. In RLHF the KL term is instead added on purpose, to keep the policy near its reference.
Over sequences the sum behind KL is intractable — but KL is an expectation, so estimate it by Monte Carlo: average the log-ratio over samples drawn from the policy.
This is the practical face of the information theory: approximate a target distribution from samples, paying for the estimate in variance rather than an intractable sum.
A preference (“response y_w is better than y_l”) is a binary outcome, so a score difference is squeezed through a sigmoid into a probability:
Three things to recognize:
Lecture 2 derives this Bradley–Terry model in full to train a reward model; Lecture 6 reuses the exact same \sigma(\Delta r) shape inside DPO.
Reinforcement learning studies an agent acting in a Markov decision process (\mathcal{S}, \mathcal{A}, P, r, \gamma):
| MDP concept | Language model |
|---|---|
| State s_t | Prompt + tokens so far: (x, y_{<t}) |
| Action a_t | Next token y_t |
| Transition P | Deterministic: append the token |
| Policy \pi_\theta(a_t \mid s_t) | LM next-token distribution |
| Reward r | Usually terminal: RM score or verifier output |
| Discount \gamma | Typically 1.0 (no discounting) |
The policy is the LM, factorized over tokens:
The goal of RL is to choose policy parameters \theta that maximize expected reward under the policy itself:
A human preference (or a verifier) is a single scalar at the end of a response:
So there is no cross-entropy target for “preferred.” Policy gradients optimize the expected evaluator score anyway — first by learning that signal as a reward model, then by optimizing it. That is the whole motivation for RLHF.
Everything in this deck is the basic machinery — probabilities, losses, gradients, and sampling. Post-training is what you get when you point that machinery at a pretrained model with richer feedback than raw next-token prediction:
Different data and different objectives; the same underlying goal:
Change the model’s distribution over responses toward behavior we want.
That is what the rest of this course is about.
Go down rabbit holes, skip around, and chase what excites you — the course is built for that.