Index Ai Academy
Phase 6 · Modern LLM engineering · Post-training

RLHF

Reward models and reinforcement learning from human feedback.

About 40 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

ChatGPT and GPT-3 started from the same kind of pretrained model. What made ChatGPT feel so different was a training step called reinforcement learning from human feedback, or RLHF. It trains the model to write the answers people prefer. Its parts now show up all over post-training: a learned score, a model pushed to raise that score, and a fine for drifting too far from where it started. Its main weakness is a model that learns to game a flawed score. That problem is one of the central puzzles in AI safety.

What you'll be able to do

  • Train a reward model on preference pairs with the Bradley-Terry loss
  • Use a reward model to pick the best of several sampled answers
  • Explain the KL-regularized objective that RLHF optimizes, and what β controls
  • Recognize reward hacking and why RLHF needs a leash on the policy

What it covers

  1. The recipe
  2. Step 2: a reward model
  3. Worked example: the loss for four pairs
  4. Training a reward model on real preferences
  5. Step 3: reinforcement learning, on a leash
  6. The best policy has a simple shape
  7. Worked example: two answers, four leashes
  8. Watching reward hacking happen
  9. How the RL step actually runs

The recipe

Picture a new cook at a restaurant. First, they copy the house recipes. Next, the owner asks diners to taste two plates and say which they like better. A food critic studies thousands of those votes until they can guess what diners will pick. Last, the cook practices. They make dishes, the critic scores them, and the cook leans toward whatever scores well. The owner adds one rule: don't stray too far from the house recipes.

RLHF follows the same three steps. OpenAI laid them out in its InstructGPT paper in 2022, and most RLHF systems still use that recipe.

  1. SFT: fine-tune the base model on example conversations. That's supervised fine-tuning, which had its own lesson earlier in this phase.
  2. Reward model: collect preference pairs, where a person picked the better of two answers. Then train a model to score answers so the preferred ones score higher. This is the critic.
  3. Reinforcement learning: the model being trained is now called the policy. It writes answers, the reward model scores them, and the policy is updated to earn higher scores. It also pays a fine for drifting too far from the SFT model.

The paper's headline result is striking. The smallest InstructGPT had 1.3 billion parameters. GPT-3 had 175 billion, more than 100 times as many. Yet labelers preferred the small InstructGPT's answers. On that test, post-training beat a hundredfold jump in size.

Try it now

The leash matters. Without it, the model finds ways to game the reward model.

Quick check
In step 3 of RLHF, who scores the answers the policy writes?
  1. A person reads and rates every single answer
  2. The reward model, which learned from people's earlier choices
  3. The SFT model
  4. Nobody: the policy grades itself
Show the answer

The reward model, which learned from people's earlier choices

People only supply the preference pairs in step 2. During step 3 the reward model stands in for them, which is what makes it fast and cheap enough to run millions of times.

Step 2: a reward model

A reward model reads a prompt and an answer and gives back one number, its score. A higher score means "people would likely prefer this one."

You train it on preference pairs with the Bradley-Terry loss from the preference-data lesson. For each pair, the model scores both answers. Write the chosen answer's score as rchosenr_{chosen}. Write the rejected answer's score as rrejectedr_{rejected}. The loss for that pair is:

−log⁡σ(rchosen−rrejected)-\log \sigma(r_{chosen} - r_{rejected})

Here σ\sigma (sigma) is the sigmoid from Phase 2. It squeezes any number into a chance between 0 and 1. The log⁡\log is the natural log, and the minus sign in front turns it into a cost.

In plain words: take the gap between the two scores. Turn the gap into the chance that the chosen answer wins. The loss is minus the log of that chance. A wide gap in the right direction gives a loss near 0. A gap in the wrong direction gives a big loss.

Training pushes the loss down over many pairs. So the model learns to give chosen answers higher scores than rejected ones, by as wide a margin as it can.

Quick check
What does this print?
rewards = {"A": 0.9, "B": 0.4}
print(max(rewards, key=rewards.get))
  1. 0.9
  2. A
  3. B
  4. 0.4
Show the answer

A

max with key=rewards.get picks the key with the highest value, so A, at 0.9, beats B. The reward model gives each answer a score, and training pushes toward the higher one.

Worked example

Worked example: the loss for four pairs

A reward model scores the chosen answer 3 and the rejected answer 1. What's the loss? Then try three more pairs: a tie, a pair it gets backward, and the first pair with 10 added to both scores.

import math

def bt_loss(r_chosen, r_rejected):
    gap = r_chosen - r_rejected
    chance = 1 / (1 + math.exp(-gap))     # sigmoid: the chance the chosen answer wins
    return -math.log(chance)

for chosen, rejected in [(3.0, 1.0), (1.0, 1.0), (1.0, 3.0), (13.0, 11.0)]:
    print(chosen, rejected, round(bt_loss(chosen, rejected), 3))
It runs right here in your browser, with nothing to install.

For the first pair the gap is 3 − 1 = 2. The sigmoid turns 2 into a chance of 0.881. Minus the log of 0.881 is 0.127, a small loss: the model already ranks this pair the right way.

A tie gives a gap of 0, a chance of 0.5 and a loss of 0.693. The backward pair has a gap of −2. Its chance is only 0.119, so the loss jumps to 2.127. Training works hardest on pairs like that.

The last pair, 13 and 11, has the same gap of 2 as the first. So it has the same loss, 0.127. Only the gap between the scores matters, never the scores themselves.

Quick check
A reward model trained on preference pairs outputs a number for each answer. What does that number mean?
  1. The number of guideline violations
  2. The probability that the answer is true
  3. Only differences matter: a higher score means 'more likely to be preferred' under Bradley-Terry
  4. A grade from 0 to 10 assigned by a person
Show the answer

Only differences matter: a higher score means 'more likely to be preferred' under Bradley-Terry

The loss depends only on r_chosen − r_rejected, so the scale and zero point are arbitrary. Adding 10 to every score changes nothing, as the worked example showed.

Training a reward model on real preferences

The practice API has a preference dataset built for this lesson and the next. The chat model, tiny-gpt-chat, wrote four answers to each of 360 product prompts. Then a brand reviewer, POST /v1/brand-check, judged each answer against four guidelines:

  • name the product that was asked about;
  • don't repeat a word in a sentence;
  • use a material the catalog uses for that kind of product;
  • mention the repair promise.

Each pair puts the best of the four answers against the worst. The reward model below never sees those guidelines. It only sees which answer in each pair won.

Once trained, the reward model is used for best-of-n sampling. Generate several answers (here, n=4n = 4), score each one, and keep the top scorer. It's like asking four people for a draft and keeping the one your editor likes most.

Here's what the code does, step by step:

  1. It loads the chat model and the 293 preference pairs. Pairs about six product names are held back for testing. The rest are for training.
  2. The features function turns each prompt and answer into a long list of numbers. It uses the chat model's hidden states, the numbers the model builds inside itself as it reads. It averages them over the answer and also keeps the ones at the answer's last token.
  3. The reward model is one nn.Linear layer, a weighted sum of those numbers. This layer is called the scoring head. It trains for 201 steps with the Bradley-Terry loss.
  4. It checks how often the reward model prefers the chosen answer in the held-back pairs.
  5. For each held-back product, it writes four answers. The real brand reviewer counts each answer's flaws. Then it compares three ways of picking one answer: take the first, take the reward model's favorite, or take the reviewer's favorite.

It's slow, so the Run button counts the seconds.

import requests
import torch
import torch.nn as nn
import torch.nn.functional as F
from tinygpt import load_pretrained

lm, tok = load_pretrained("tiny-gpt-chat")
end = tok.token_id("<end>")
pairs = requests.get("https://api.gradient.test/v1/preferences").json()["data"]
held_out = {"Kestrel", "Lark", "River", "Sierra", "Summit", "Willow"}      # product names kept for testing
train = [p for p in pairs if p["prompt"].split()[2] not in held_out]
test = [p for p in pairs if p["prompt"].split()[2] in held_out]
print(f"{len(pairs)} preference pairs: {len(train)} to train on, {len(test)} held out")

@torch.no_grad()
def features(prompts, answers):
    # The chat model's final hidden states, averaged over the answer and taken at its last token.
    rows = [(tok.encode(f"<user> {p} <assistant>"), tok.encode(a) + [end]) for p, a in zip(prompts, answers)]
    rows = [((p + a)[:64], len(p)) for p, a in rows]
    T = max(len(ids) for ids, _ in rows)
    h = lm.hidden(torch.tensor([ids + [end] * (T - len(ids)) for ids, _ in rows]))
    in_answer = torch.tensor([[float(start <= t < len(ids)) for t in range(T)] for ids, start in rows]).unsqueeze(-1)
    last = h[torch.arange(len(rows)), torch.tensor([len(ids) - 1 for ids, _ in rows])]
    return torch.cat([(h * in_answer).sum(dim=1) / in_answer.sum(dim=1), last], dim=-1)

def side(ps, which):
    return features([p["prompt"] for p in ps], [p[which] for p in ps])

torch.manual_seed(0)
reward = nn.Linear(2 * lm.head.weight.shape[1], 1)          # the reward model's scoring head
chosen, rejected = side(train, "chosen"), side(train, "rejected")
opt = torch.optim.Adam(reward.parameters(), lr=1e-2)
for step in range(201):
    loss = -F.logsigmoid(reward(chosen) - reward(rejected)).mean()       # Bradley-Terry
    opt.zero_grad()
    loss.backward()
    opt.step()
with torch.no_grad():
    accuracy = (reward(side(test, "chosen")) > reward(side(test, "rejected"))).float().mean().item()
print(f"reward model prefers the chosen answer in {accuracy:.0%} of held-out pairs")

# best-of-4 on the held-out products, judged by the real brand reviewer
kinds = sorted({" ".join(p["prompt"].split()[3:-1]) for p in pairs})
torch.manual_seed(1)
picked = {"first answer": [], "reward model's best of 4": [], "reviewer's best of 4": []}
for name in sorted(held_out):
    for kind in kinds:
        ids = torch.tensor([tok.encode(f"<user> Describe the {name} {kind} . <assistant>")] * 4)
        out = lm.generate(ids, 60, temperature=0.8, stop_id=end)
        answers = [tok.decode(row).split("<end>")[0].strip() for row in out[:, ids.shape[1]:].tolist()]
        review = requests.post("https://api.gradient.test/v1/brand-check",
                               json={"items": [{"product": f"{name} {kind}", "text": a} for a in answers]}).json()["results"]
        flaws = [len(r["flaws"]) for r in review]
        with torch.no_grad():
            scores = reward(features([f"Describe the {name} {kind} ."] * 4, answers)).squeeze(-1)
        picked["first answer"].append(flaws[0])
        picked["reward model's best of 4"].append(flaws[int(scores.argmax())])
        picked["reviewer's best of 4"].append(min(flaws))
for how, f in picked.items():
    print(f"{how:<26} {sum(f) / len(f):.2f} guideline violations per answer")

The reward model agrees with the reviewer on 69% of the pairs it never trained on. That's in the range real reward models reach against held-out human judgments. It's a sobering number. The "reward" that RLHF pushes up is a noisy imitation of what people want.

Best-of-4 with the reward model cuts the flaws from 1.78 to 1.39 per answer. Picking with the reviewer itself would reach 0.99. The gap between 1.39 and 0.99 is the cost of the reward model's mistakes.

This reward model is a cheap version. It only trains a scoring head on top of the chat model's frozen hidden states. Production reward models are usually a full copy of the SFT model with a scoring head, and all of it is fine-tuned. (torch here is the course's in-browser PyTorch subset.)

Quick check
Best-of-4 using the reward model averaged 1.39 flaws per answer. Picking with the reviewer itself averaged 0.99. Why is the reward model's pick worse?
  1. It's an imperfect copy of the reviewer (69% agreement on held-out pairs), so it sometimes picks a worse answer
  2. It only looked at the first answer
  3. The reviewer saw more answers than the reward model
  4. Best-of-n always makes answers worse
Show the answer

It's an imperfect copy of the reviewer (69% agreement on held-out pairs), so it sometimes picks a worse answer

Both chose from the same four answers. The reward model learned the reviewer's taste from pairs alone, and it gets that taste wrong some of the time.

Step 3: reinforcement learning, on a leash

Best-of-n improves answers, but every request now costs nn answers instead of one. RLHF goes further. It trains the policy to write high-scoring answers in the first place.

There's a catch. The reward model is only about 70% right here, and a training process hunts for anything that raises the score. Left free, it will find the reward model's mistakes and use them. So RLHF keeps the policy on a leash, like a dog on a walk. The dog can explore, but only so far from you. Here, "you" is the SFT model.

The goal can be written as one line of math. First, the symbols, one at a time:

  • xx is the prompt.
  • yy is an answer the model writes.
  • π\pi (the Greek letter pi) is the policy. Here it has nothing to do with 3.14.
  • πref\pi_{\text{ref}} is the reference model: the SFT model, kept frozen.
  • r(x,y)r(x, y) is the reward model's score for answer yy to prompt xx.
  • Ey∼π\mathbb{E}_{y \sim \pi} means "the average over answers the policy writes."
  • KL\mathrm{KL} is the KL divergence from Phase 2. It measures how far apart two sets of chances are, and it's 0 when they match.
  • β\beta (beta) is a number you choose. It sets how tight the leash is.
max⁡π  Ey∼π[r(x,y)]−β KL(π ∥ πref)\max_{\pi} \; \mathbb{E}_{y \sim \pi}\big[r(x, y)\big] - \beta \, \mathrm{KL}\big(\pi \,\|\, \pi_{\text{ref}}\big)

In plain words: find the policy with the best average score after you take off a fine for drifting from the SFT model. The fine is the drift times β\beta. A big β\beta means a short leash. A small β\beta lets the policy chase the score.

Quick check
What does the KL term in the goal do?
  1. It speeds up sampling
  2. It raises the reward
  3. It removes bad training data
  4. It fines the policy for drifting too far from the SFT model
Show the answer

It fines the policy for drifting too far from the SFT model

That's the leash. Without it, the policy learns to exploit weaknesses in the reward model.

The best policy has a simple shape

This goal has an exact answer. Call the best policy π∗\pi^* (pi star). For one fixed prompt, the score depends only on the answer, so it's written r(y)r(y).

π∗(y)∝πref(y) er(y)/β\pi^*(y) \propto \pi_{\text{ref}}(y)\, e^{r(y)/\beta}

Here ee is the number from the exponentials and logarithms lesson, about 2.718. The symbol ∝\propto means "is proportional to." You compute the right side for every answer. Then you divide each result by their total, so the chances add up to 1.

In plain words: start from how often the SFT model writes each answer. Multiply by a boost that grows with the answer's score. Then rescale so the chances add up to 1. Dividing the score by β\beta sets how strong the boost is.

Real models can't use this formula directly, because there are far too many possible answers to list. On a small made-up example, though, it shows exactly what β\beta does.

Go deeper Why that formula is the best policy

Let ZZ be the total you divide by in the formula above, so the chances in π∗\pi^* add up to 1. With a little algebra, the goal for any policy π\pi can be rewritten as:

Ey∼π[r(y)]−β KL(π ∥ πref)=βlog⁡Z−β KL(π ∥ π∗)\mathbb{E}_{y \sim \pi}\big[r(y)\big] - \beta \, \mathrm{KL}\big(\pi \,\|\, \pi_{\text{ref}}\big) = \beta \log Z - \beta \, \mathrm{KL}\big(\pi \,\|\, \pi^*\big)

Read it as: the goal equals a fixed amount, minus β\beta times how far π\pi is from π∗\pi^*. The first part doesn't depend on π\pi at all. A KL divergence is never negative, and it's 0 only when the two sets of chances match. So the goal is highest when π\pi equals π∗\pi^*.

To get there, write out the KL from π\pi to π∗\pi^* and swap in the formula for π∗\pi^*. The log of er/βe^{r/\beta} is r/βr/\beta, which pulls the reward out as its own term. Multiply through by β\beta and rearrange.

Worked example

Worked example: two answers, four leashes

The SFT model writes a "plain" answer 80% of the time and a "punchy" one 20% of the time. The reward model scores plain 0 and punchy 1. How often does the best policy write each, at β = 10, 1, 0.5 and 0.25?

import math

p_ref = [0.8, 0.2]      # how often the SFT model writes "plain" and "punchy"
reward = [0.0, 1.0]     # the reward model's score for each
for beta in [10, 1, 0.5, 0.25]:
    weights = [p * math.exp(r / beta) for p, r in zip(p_ref, reward)]
    total = sum(weights)
    print(beta, [round(w / total, 3) for w in weights])
In the lesson you can change this and run it.

Take β=1\beta = 1. Plain gets 0.8 × e⁰ = 0.8. Punchy gets 0.2 × e¹ = 0.2 × 2.718 = 0.544. The total is 1.344. Dividing by it gives plain 0.595 and punchy 0.405. Punchy's share has about doubled.

At β=10\beta = 10 the boost is tiny, so the policy stays close to the SFT model: punchy only rises from 0.2 to 0.216. At β=0.25\beta = 0.25 the score is divided by a small number, so the boost is huge. Punchy jumps to 0.932.

That's the leash. A long leash, meaning a small β\beta, lets the reward model's opinion take over. That's fine when the reward model is right, and harmful when it's wrong.

Watching reward hacking happen

Now try five possible answers. One of them games the reward model: it repeats the repair promise three times. The reward model loves that, and gives it the top score of 1.6. The brand team hates it. So the code keeps two lists of scores, the reward model's and the brand team's true opinion. For each β\beta, it builds the best policy with the formula above and prints:

  • the average reward model score;
  • the average true quality;
  • the KL, how far the policy has drifted from the SFT model;
  • how often the policy writes the gaming answer.
import numpy as np

answers = ["on brand, clean", "on brand, plain", "repeats a word", "wrong material", "repair promise three times"]
p_ref = np.array([0.30, 0.35, 0.15, 0.15, 0.05])     # how likely the SFT model is to write each
reward = np.array([1.0, 0.7, -0.5, -0.8, 1.6])       # the reward model's score
true = np.array([1.0, 0.7, -0.5, -0.8, -1.0])        # what the brand team actually thinks

print(f"{'beta':>5}  {'reward score':>12}  {'true quality':>12}  {'KL':>5}   share of the gaming answer")
for beta in [10, 2, 1, 0.5, 0.25, 0.1]:
    p = p_ref * np.exp(reward / beta)                 # the optimal policy for this beta
    p /= p.sum()
    kl = float((p * np.log(p / p_ref)).sum())
    print(f"{beta:>5}  {p @ reward:>12.2f}  {p @ true:>12.2f}  {kl:>5.2f}   {p[4]:.0%}")

Read the table from top to bottom, as β\beta shrinks and the leash gets longer. The reward model's score climbs the whole way, from 0.48 to 1.59. True quality rises at first and peaks at 0.50 when β=1\beta = 1. Then it collapses to −0.97. By then the policy writes the gaming answer 98% of the time.

This is called reward hacking, or over-optimization. It's Goodhart's law in action: "when a measure becomes a target, it ceases to be a good measure." A school that's judged only on test scores can start teaching to the test. The scores go up, and the learning doesn't.

Researchers at OpenAI measured the same shape in real models. They used a much larger "gold" reward model as a stand-in for people. As training pushed the policy further from the reference, the trained reward model's score kept rising. The gold score peaked and then fell.

Quick check
In the table, β drops from 1 to 0.1. What happens to the reward model's score and to true quality?
  1. Both rise together
  2. Both fall together
  3. The reward model's score falls, while true quality rises
  4. The reward model's score keeps rising, while true quality falls
Show the answer

The reward model's score keeps rising, while true quality falls

The score goes from 0.83 to 1.59, while true quality drops from 0.50 to −0.97. A proxy score that rises while real quality falls is the sign of reward hacking.

How the RL step actually runs

With a real model, you can't list every possible answer, so you can't use the formula. Instead, RLHF uses a training method from reinforcement learning. The classic choice is PPO, short for proximal policy optimization. Each round goes like this:

  1. The policy writes a batch of answers.
  2. The reward model scores each one.
  3. The KL fine is subtracted token by token, so each token that drifts from the SFT model costs a little.
  4. The policy is nudged toward answers that scored better than expected. "Expected" comes from a fourth model, the value model. It predicts the score an answer is likely to get, from the part written so far.
  5. Each update is clipped, meaning capped, so no single round changes the policy too much.

That means four models in memory at once: the policy, the frozen reference, the reward model and the value model. It also means lots of answer writing and many settings to tune. PPO works, but it's famously fussy.

That's why the next lesson's method, DPO, became popular. It skips the reward model and the answer writing. It's also why reasoning models are trained with simpler variants, such as GRPO. Those use rewards you can check, like whether a math answer is right, instead of a learned reward model.

For marketers: you can use best-of-n with a scorer today, without training anything. Write several ad variants. Score them with a rubric-based LLM judge or a classifier, and keep the best. Spot-check the winners, though. A scorer can be gamed by copy that hits its keywords without being good.

Quick check
In PPO, what job does the value model do?
  1. It writes the answers that get scored
  2. It predicts the score an answer will get, so the policy can favor answers that beat that guess
  3. It stays frozen as the reference the KL fine is measured against
  4. It collects new preference pairs from people
Show the answer

It predicts the score an answer will get, so the policy can favor answers that beat that guess

The policy writes the answers and the frozen SFT model is the reference. The value model's guess is the "expected" score, and answers that beat it get reinforced.

Practice what you just read

  • 4 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 6 review cards for spaced repetition

Create a free account

Try a quiz question

Why does RLHF penalize the KL divergence from the SFT model?

  1. The reward model is imperfect; without a leash the policy drifts toward answers that exploit its mistakes
  2. KL makes training faster, because the policy only has to learn the small differences from where it started
  3. To keep the policy's vocabulary unchanged, because adding or removing tokens would break the reward model
  4. Because the reward model only works on short answers, and staying close to SFT keeps replies the same length

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson