Instruction and preference data
The datasets that shape assistant behavior.
Create a free account Try part of it here
Why it matters
A chat model learns to act like an assistant from its post-training data. That data is a set of examples people wrote, plus many choices people made between two answers. Those examples and choices set the model's tone and how long its replies are. They decide what it refuses and when it flatters you. If you fine-tune a model, grade outputs with an LLM judge, or wonder why assistants act the way they do, the data and its quirks are where to look.
What you'll be able to do
- Describe the three main kinds of post-training data and how each is collected
- Turn pairwise preferences into scores with the Bradley-Terry model
- Measure annotator agreement and detect position bias in judgments
- Recognize the biases that preference data passes on to models
What it covers
- Three kinds of data
- From comparisons to scores: Bradley-Terry
- Fitting strengths from votes
- Do the annotators agree?
- Does the order matter?
- Worked example: two reviewers, ten tagline pairs
- Where preferences come from
- The dataset for the next two lessons
Three kinds of data
Think of training a new cook. You can show them finished dishes to copy. You can let them taste two versions of a sauce and say which is better. Or you can give them tasks with a clear right answer, like "weigh out 500 grams of flour", which a scale can check. Post-training data comes in the same three kinds.
- Demonstrations, used for supervised fine-tuning (SFT, the last lesson). Each one is a prompt and an ideal response. People write them, or a stronger model drafts them and people edit. OpenAI's InstructGPT (2022) started from about 13,000 prompts with responses written by its contractors. Today many datasets are mostly written by models and then filtered.
- Preferences. Each one is a prompt, two or more responses, and a note of which is better. For InstructGPT, labelers ranked several responses to each of about 33,000 prompts. Anthropic released about 170,000 human comparisons, called the HH-RLHF dataset. Preferences train a reward model (RLHF, the next lesson) or train the model directly (DPO, the lesson after).
- Checkable tasks. These are problems with an answer a computer can verify: math with a known result, or code with tests. No person has to judge each example. That's why they scale so well for training reasoning models.
Datasets are usually stored as JSON lines: a text file with one example per line. Each line looks like a Python dictionary. The first line below is a demonstration. The second is a preference pair: one prompt, a chosen answer and a rejected one.
{"messages": [{"role": "user", "content": "Describe the Kestrel tent."}, {"role": "assistant", "content": "Our Kestrel tent is made from recycled ripstop nylon. It is roomy and fully waterproof."}]}
{"prompt": "Describe the Kestrel tent.", "chosen": "Our Kestrel tent is made from recycled ripstop nylon. It is roomy and fully waterproof.", "rejected": "Our Kestrel tent is made from enamel steel. It is roomy, roomy and roomy."}
Why compare two answers?
You could ask people to score one answer from 1 to 10. But people are bad at steady scores. Is this a 6 or a 7? You might say 6 today and 7 tomorrow. But almost anyone can tell you which of two coffees they like better. Comparisons are far more reliable. They also capture qualities that are hard to define but easy to spot: clearer, kinder, more on-brand.
Show the answer
Checkable tasks
A computer can check each answer by running the tests, so no person has to judge it. Demonstrations would need an ideal written answer. Preferences would need a choice between answers. JSON lines is a file format, not a kind of data.
From comparisons to scores: Bradley-Terry
Chess players get a rating from the games they win and lose. Nobody plays everyone, yet the ratings let you predict any matchup. You can do the same for answers, models or ad taglines with the Bradley-Terry model.
The idea: give each option one number, its strength. When two options meet, the bigger the gap in strength, the more often the stronger one wins. Equal strengths mean a coin toss.
Now the formula. Call one option . The letter is only a label, like "Writer A". Call the other option . Write for the strength of option . In the same way, is the strength of option . The stands for probability, the chance that something happens. The chance that option beats option is:
Here (sigma) is the sigmoid function from the logistic regression lesson. It squashes any number into a chance between 0 and 1. The letter is a fixed number, about 2.718.
In plain words: subtract one strength from the other, then feed the gap into the sigmoid. A gap of 0 gives 50%. A big positive gap gives nearly 100%. A big negative gap gives nearly 0%.
Worked example. Writer A has strength 0.8 and Writer D has −0.8. The gap is 0.8 − (−0.8) = 1.6. Next, is about 0.20. So the chance is 1 / (1 + 0.20), which is about 0.83. Writer A should win about 83 of every 100 matchups against Writer D.
import math s_a, s_d = 0.8, -0.8 # strengths of Writer A and Writer D p = 1 / (1 + math.exp(-(s_a - s_d))) # the Bradley-Terry formula print(round(p, 2))
This is how leaderboards like LMArena (formerly Chatbot Arena) rank chat models from millions of head-to-head votes. In the next lesson, it's also how a reward model learns from preference pairs.
import math a, b = 1.0, 1.0 print(1 / (1 + math.exp(b - a)))
Show the answer
0.5
b − a is 0, and e to the power of 0 is 1, so it's 1 / (1 + 1) = 0.5. Equal strengths mean a coin toss.
A gap of 0 means a coin flip. A gap of 1 means about 73%. Comparisons turn into a single score for each answer.
Fitting strengths from votes
In real life you don't know the strengths. You only see who won. Fitting means finding the strengths that best explain the votes. The cell below runs a small contest. Four copywriters' taglines are compared 800 times by simulated raters. The code knows each writer's hidden true strength, so you can see how close the fit gets. It does three things:
- It simulates 800 votes, using the formula above to pick each winner.
- It prints how often each writer beat each of the others.
- It starts every strength at 0 and nudges them 500 times, until they explain the votes well.
import numpy as np rng = np.random.default_rng(0) writers = ["Writer A", "Writer B", "Writer C", "Writer D"] true_strength = np.array([1.0, 0.4, 0.0, -0.6]) # hidden quality on a logit scale votes = [] # (winner, loser) pairs for _ in range(800): i, j = rng.choice(4, size=2, replace=False) p_i_wins = 1 / (1 + np.exp(-(true_strength[i] - true_strength[j]))) votes.append((i, j) if rng.random() < p_i_wins else (j, i)) wins = np.zeros((4, 4)) for w, l in votes: wins[w, l] += 1 print("win rate of row against column:") for i in range(4): cells = [f"{wins[i, j] / (wins[i, j] + wins[j, i]):.2f}" if i != j else " - " for j in range(4)] print(f" {writers[i]} " + " ".join(cells)) s = np.zeros(4) # fit by gradient ascent on the log-likelihood W, L = np.array(votes).T for step in range(500): p = 1 / (1 + np.exp(-(s[W] - s[L]))) # predicted chance each recorded winner wins grad = np.zeros(4) np.add.at(grad, W, 1 - p) np.add.at(grad, L, -(1 - p)) s += 0.01 * grad s -= s.mean() # only differences matter; center at 0 print("\nfitted strength:", [round(float(v), 2) for v in s]) print("true strength: ", [round(float(v), 2) for v in true_strength - true_strength.mean()]) print("Elo-style rating:", [int(round(1000 + 400 / np.log(10) * v)) for v in s])
The fitted strengths, [0.87, 0.17, −0.16, −0.88], land close to the true ones, [0.8, 0.2, −0.2, −0.8]. The code shifts both lists so they average 0, because only the gaps matter. Writer A beat Writer D in 85% of their votes, close to the 83% you worked out above. And four numbers explain all six matchups at once. Nobody had to compare every pair many times.
The last line shows Elo-style ratings, the kind used in chess. They're the same strengths on a different scale: 1000 plus about 174 times the strength. On that scale, a gap of 400 points means 10-to-1 odds. The stronger side wins about 10 games for every 1 it loses.
Go deeper How the fitting loop works
Fitting Bradley-Terry is the same job as fitting a logistic regression. The loop climbs the log-likelihood, a score for how well the strengths explain the votes. In plain steps:
- For every recorded vote, it works out
p, the chance the current strengths give the winner. - If
pis low, the result was a surprise. So the winner gets a nudge up of1 - p, and the loser a nudge down of the same size. A vote the strengths already predicted well barely moves them. - It adds up these nudges for each writer and takes a small step: 0.01 times the total.
- Then it subtracts the average strength, so the strengths stay centered on 0. Adding the same amount to every strength wouldn't change any gap. This only fixes where zero sits.
np.add.at does the adding up. A writer appears in many votes, and np.add.at counts every one of them. You'll write this loop yourself in the exercise.
Show the answer
About 73% of the time
The gap is 1.0 − 0.0 = 1. Then σ(1) = 1 / (1 + e⁻¹) ≈ 0.73. A strength gap of 1 is an edge, not a sure win.
Do the annotators agree?
Preference labels are noisy. People get tired, rush, or honestly disagree. So teams run two checks on their labels. The first is agreement. Give the same pairs to two annotators (the people who label the data) and count how often they pick the same answer.
Raw agreement makes the data look better than it is. Two people flipping coins would agree half the time. Cohen's kappa corrects for that luck. It's written , the Greek letter kappa. It uses two numbers:
- : how often the two annotators really agree.
- : how often they'd agree by luck, if each guessed at their usual rates.
In plain words: take how far the annotators beat luck. Divide it by the most they could have beaten luck. A kappa of 0 means no better than chance. A kappa of 1 means they always agree.
Worked example. Two annotators agree on 75% of pairs. Luck alone would give 50%. They beat luck by 0.75 − 0.50 = 0.25. The most they could have beaten it by is 1 − 0.50 = 0.50. So kappa is 0.25 / 0.50 = 0.5.
p_agree = 0.75 # how often the two annotators agree p_chance = 0.50 # how often they'd agree by luck kappa = (p_agree - p_chance) / (1 - p_chance) print(kappa)
For comparison, InstructGPT's labelers agreed with each other about 73% of the time.
Show the answer
0
(0.7 − 0.7) / (1 − 0.7) = 0. They agree no more often than luck would make them, so the 70% says nothing good about the labels.
Does the order matter?
The second check is for position bias. Judges tend to favor whichever response is shown first (or, for some judges, second). People do it, and LLM judges do it even more. It's like a taste test where the first sample often wins because it came first.
To test for it, show every pair twice, once in each order. Say answer X comes first, then Y. Next time, Y comes first. A fair judge that prefers X picks "first" once and "second" once. If it picks "first" both times, the order changed its verdict.
The cell below runs both checks on made-up data. It makes 300 pairs, some easy to judge and some close calls. Two careful annotators label them all. Then a biased judge labels every pair in both orders. Three times in ten, this judge skips the reading and takes whichever response came first.
import numpy as np rng = np.random.default_rng(1) n = 300 first_is_better = rng.random(n) < 0.5 # hidden truth for each pair, as first shown ease = rng.random(n) # some pairs are easy to judge, some are close calls def picks_first(truth, position_bias=0.0): right = rng.random(n) < 0.55 + 0.4 * ease # easy pairs judged right ~95%, close calls ~55% choice = np.where(right, truth, ~truth) lazy = rng.random(n) < position_bias # sometimes just takes whichever came first return np.where(lazy, True, choice) a, b = picks_first(first_is_better), picks_first(first_is_better) agree = (a == b).mean() chance = a.mean() * b.mean() + (1 - a.mean()) * (1 - b.mean()) print(f"two annotators agree on {agree:.0%} of pairs (chance alone: {chance:.0%}); Cohen's kappa {(agree - chance) / (1 - chance):.2f}") print(f"on the easiest third they agree on {(a == b)[ease > 2 / 3].mean():.0%}, on the hardest third {(a == b)[ease < 1 / 3].mean():.0%}") # a judge with a position bias, shown every pair twice: original order, then swapped order1 = picks_first(first_is_better, position_bias=0.3) order2 = picks_first(~first_is_better, position_bias=0.3) # the other response is now first print(f"\njudge picks whichever response is shown first {np.concatenate([order1, order2]).mean():.0%} of the time") print(f"its verdict changes when the order is swapped on {(order1 == order2).mean():.0%} of pairs")
The two annotators agree on 64% of pairs, against 50% by chance, so kappa is only 0.29. Most of the disagreement comes from close calls. On the easiest third of pairs they agree 71% of the time. On the hardest third it's 55%, barely above a coin flip. That's realistic. Much disagreement comes from real ambiguity, not carelessness. Clear guidelines reduce it. Teams also drop disputed pairs or give them less weight.
The biased judge picks the first response 67% of the time. A fair judge would pick it about half the time. Its verdict changes on 50% of pairs once the order is swapped. The usual fix for LLM judges is to judge both orders and average them, or to shuffle the order.
Go deeper How the simulation is built
Each pair gets a hidden truth (is the first response better?) and an "ease" between 0 and 1. A careful annotator judges a pair correctly with chance 0.55 + 0.4 × ease. So an easy pair (ease near 1) is judged right about 95% of the time, and a close call (ease near 0) about 55%. The biased judge works the same way, except that 30% of the time it picks the first response without looking.
The kappa line uses the formula above. The chance term comes from each annotator's own rate of picking "first". Both might pick "first" by luck, or both might pick "second" by luck. Add those two chances and you get the chance of agreeing by luck.
Show the answer
The order, not the content, decided at least one verdict
Each response was first once. Picking "first" both times means the judge chose X once and Y once. A consistent judge picks "first" in exactly one of the two orders.
Worked example: two reviewers, ten tagline pairs
Ana and Ben each judged the same 10 pairs of ad taglines. A 1 means they preferred the tagline shown first. A 0 means they preferred the second. Work out their raw agreement, the agreement you'd expect by luck, and Cohen's kappa.
ana = [1, 1, 0, 1, 0, 0, 1, 1, 0, 1] # 1 = preferred the tagline shown first ben = [1, 0, 0, 1, 0, 1, 1, 1, 0, 1] agree = sum(a == b for a, b in zip(ana, ben)) / len(ana) ana_first = sum(ana) / len(ana) ben_first = sum(ben) / len(ben) chance = ana_first * ben_first + (1 - ana_first) * (1 - ben_first) kappa = (agree - chance) / (1 - chance) print("agree:", agree) print("chance:", round(chance, 2)) print("kappa:", round(kappa, 2))
They agree on 8 of the 10 pairs, so raw agreement is 0.8. Each of them picks "first" 6 times in
- By luck, they'd both pick "first" 0.6 × 0.6 = 0.36 of the time. They'd both pick "second"
0.4 × 0.4 = 0.16 of the time. So chance agreement is 0.36 + 0.16 = 0.52. Kappa is (0.8 − 0.52) / (1 − 0.52) = 0.28 / 0.48, about 0.58. The 80% looked strong. Once luck is taken out, the agreement is only moderate.
Where preferences come from
- Trained annotators follow written guidelines: be helpful, honest and harmless; follow the format; don't make things up. The guidelines are, in effect, the specification of the model.
- AI feedback. A strong model judges pairs against written principles. You'll meet this as Constitutional AI in a later lesson. It's cheaper and more consistent than people. But it inherits the judge model's biases.
- User signals. Thumbs up or down, and "which response do you prefer?" prompts in chat apps. These come in huge volume. They are also very noisy and lean toward what feels good in the moment.
What preferences pass on
A model trained on preference data picks up the biases in that data. Three show up again and again:
- Length bias. Raters often prefer longer answers that look thorough. Models drift toward wordy replies unless length is controlled for.
- Sycophancy, a word for flattery. People prefer answers that agree with them and praise them. Models trained on those choices learn to tell people what they want to hear.
- Confident style. Fluent, sure-sounding answers win comparisons even when they're wrong, unless raters check the facts.
For marketers: say you use an LLM judge to pick the best of several ad variants. Show each pair in both orders. Check how often it agrees with a sample of human picks. And watch whether it keeps choosing the longer copy.
The dataset for the next two lessons
The practice API has a real, if small, preference dataset. It comes from tiny-gpt-chat, the chat model from the last two lessons. The model wrote four answers to each of 360 "Describe the …" prompts. A brand reviewer then judged every 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 in the dataset is the best of the four answers against the worst. The reviewer is code, not a person. You can call it too, as POST /v1/brand-check, the way you'd call an LLM judge. The cell below loads the pairs and shows one. It runs the brand check on that pair's rejected answer. Then it counts which guidelines the chosen and rejected answers break.
import collections import requests data = requests.get("https://api.gradient.test/v1/preferences").json() print(f"{data['count']} pairs, for example:") pair = data["data"][24] print(" prompt: ", pair["prompt"]) print(" chosen: ", pair["chosen"]) print(" rejected:", pair["rejected"]) review = requests.post("https://api.gradient.test/v1/brand-check", json={"product": "Basin headlamp", "text": pair["rejected"]}).json() print("\nbrand check on the rejected answer:", review["flaws"]) def rule(flaw): if flaw.startswith("doesn't name"): return "names another product" if flaw.startswith("repeats"): return "repeats a word" if flaw == "no repair promise": return "no repair promise" return "wrong or no material" for side in ("chosen", "rejected"): counts = collections.Counter(rule(f) for p in data["data"] for f in p[side + "_flaws"]) print(f"\nviolations in {side} answers:", dict(counts.most_common()))
Even the chosen answers aren't perfect. Of the 293 chosen answers, 163 still miss the repair promise and 74 name another product. That's typical. Real preference data records which answer is better, not which is good.
The gap between the two sides depends on the guideline, though. Nearly every rejected answer (279) lacks the repair promise. That's a fixed phrase, easy to spot. Naming the right product takes a skill the model only partly has. Keep that in mind for the DPO lesson.
Show the answer
A chosen answer is only better than the rejected one, not necessarily good
Each chosen answer only had to beat the worst of four. It can still break guidelines. That's normal for preference data: it records which answer is better, not which is good.
Practice what you just read
- 6 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
Why do post-training teams usually collect pairwise comparisons instead of 1-to-10 scores?