Direct Preference Optimization
Direct Preference Optimization (DPO) is a technique for aligning language models with human preferences, without needing reinforcement learning. It replaces the traditional RLHF pipeline with a single supervised fine-tuning step and a clever loss function.
Overview
Imagine you’ve built a chatbot that can write text, but it sometimes says unhelpful or weird things. You want to teach it to respond the way humans actually prefer. The question is: how do you do that efficiently?
The old way (RLHF) had three complicated steps. First, you show humans two responses to the same question and ask “which one is better?” to collect preference data. Second, you build a separate “judge” model (a reward model) that learns to score responses the way humans would. Third, you use reinforcement learning to nudge your chatbot toward getting higher scores from that judge. This whole pipeline is expensive, fragile, and hard to get right.
The DPO breakthrough: the authors discovered a mathematical shortcut. They showed that you can collapse all three steps into one. Instead of building a separate judge and then doing the complicated RL dance, you can directly adjust the chatbot using the human preference data alone. Skip the middleman. The training becomes as simple as “make the preferred response more likely and the dispreferred response less likely,” with some clever math to keep things stable.
Before DPO, aligning a chatbot with human preferences required a complicated three-stage pipeline. DPO replaced it with a single, simple training step that works just as well or better. That’s why it became so widely adopted so quickly.
The Cast of Characters
- \pi_{ref}(y \mid x), the Reference Model. A frozen snapshot of the chatbot taken before training starts. It acts like a safety anchor: during training, we say "you can improve, but don't drift too far from how you originally behaved." This prevents the model from going off the rails. \pi_{ref}(y \mid x) is the probability this model assigns to generating response y given prompt x. For example, given prompt x = "What's the capital of France?": \pi_{ref}(\text{"Paris"} \mid x) = 0.4, \pi_{ref}(\text{"The capital is Paris."} \mid x) = 0.3, \pi_{ref}(\text{"I like cheese"} \mid x) = 0.001.
- \pi_\theta(y \mid x), the Policy (the Model We're Training). "Policy" just means "the chatbot and how it decides what to say next." When we "optimize the policy," we're just making the chatbot better at responding. Same architecture as the reference, but these are the weights \theta we update. Starts as a copy of \pi_{ref} and gradually changes.
- r(x, y), the Reward. A scalar: how good is response y for prompt x. Higher is better. In RLHF, a separate AI (the reward model) plays judge at a talent show, rating outputs as good or bad. DPO's big move is eliminating this entirely.
- \beta, the Leash. A hyperparameter (e.g. 0.1 or 0.5) controlling how far the trained model can stray from the reference. High \beta = stay close. Low \beta = chase reward.
- D_{KL}, KL Divergence. A way to measure how different two distributions are. Here it measures how far the chatbot has drifted from its original self. DPO uses this as a leash: if the model strays too far, the math pulls it back.
- \sigma(\cdot), the Sigmoid. Squashes any number to [0, 1]. \sigma(z) = \frac{1}{1 + e^{-z}}.
- y_w and y_l, Winner and Loser. y_w is the human-preferred response, y_l is the rejected one.
- \succ, "is preferred to." p(A \succ B) = probability that A beats B.
The Goal
Find parameters \theta that maximize expected reward:
The problem: without constraints, the model games the reward. If the reward model likes responses starting with “ABSOLUTELY! GREAT QUESTION!”, the LLM learns to say that every time.
The KL Constraint
Fix: penalize the model for straying too far from the reference. The KL divergence acts as a leash: if the model drifts too far from its original self, the math pulls it back.
D_{KL}[P | Q] (KL Divergence) measures how different two distributions are. Zero when identical, larger as they diverge.
The Optimal Policy
Instead of searching for the answer through thousands of trial-and-error steps (which is what RL does), the math gives you a direct formula. It’s the difference between solving an equation on paper versus guessing and checking repeatedly. The closed-form solution to the constrained objective:
For each response, take the reference probability and multiply by a boost factor \exp(r/\beta). High reward = boost. Low reward = shrink. Then normalize by Z(x).
The Boost Factor
| Response | π_ref | Reward | Boost | π* |
|---|---|---|---|---|
| "The capital is Paris." | 0.30 | 2.5 | ||
| "Paris" | 0.40 | 2.0 | ||
| "I like cheese" | 0.001 | -1.0 |
Z(x) = \sum_{y} \pi_{ref}(y \mid x) \cdot \exp(r(x,y)/\beta) sums over every possible string the LLM could produce. That’s infinite. You can’t compute it directly.
The Rearrangement Trick
Rearrange the optimal policy equation to express reward in terms of the policy:
The reward equals how much the optimal model prefers a response relative to the reference (times \beta), plus a constant.
The Bradley-Terry Model
Before we get to the cancellation, we need to talk about how we model human preferences. The Bradley-Terry model is surprisingly simple. It was introduced in 1952 by Ralph Bradley and Milton Terry as a way to rank items from pairwise comparisons. Think chess ratings, taste tests, or any setting where you compare two things and pick a winner.
The idea: each item i has a latent “strength” s_i. The probability that item i beats item j is:
If you parameterize strengths as exponentials of scores, s_i = e^{r_i}, this becomes:
That’s it. The probability that i beats j is just the sigmoid of the difference in their scores. The formula is simple on purpose. The power is in the statistical machinery for fitting it to messy, incomplete real-world comparison data.
Why Bradley-Terry Matters
The value isn’t in the formula itself. Imagine you have 100 chess players and a messy pile of game results where not everyone has played everyone. Bradley-Terry gives you a principled way to estimate a single strength number for each player from incomplete pairwise comparisons using maximum likelihood estimation. You can then rank all 100 players on a single scale, even if player 1 never faced player 87.
That’s surprisingly hard to do well without a model like this. Simple win percentages don’t work because schedules differ: someone who only played weak opponents would look artificially strong. Elo ratings are actually a special case of Bradley-Terry, so if you’ve ever looked at chess ratings, you’ve already been using this model.
Applied to LLM alignment: given a prompt x and two responses y_1, y_2 with rewards r(x, y_1) and r(x, y_2):
Human preference depends only on the difference in rewards. This is the property that makes DPO possible.
The Reward Model Loss (What DPO Replaces)
In RLHF, you train a neural network r_\phi (where \phi are its learnable weights) to approximate the ideal reward function. The loss:
A concrete example. Say one data point is:
- Prompt x: “Explain gravity”
- y_w (winner): “Gravity is the force that attracts objects with mass toward each other”
- y_l (loser): “Gravity is like when stuff falls down because the earth is big”
The reward model scores both: r_\phi(x, y_w) = 1.8, r_\phi(x, y_l) = 1.2.
Then: difference = 0.6, \sigma(0.6) = 0.645, \log(0.645) = -0.439, negate: loss = 0.439.
If the model had given the winner a much higher score (say difference of 5), \sigma(5) \approx 0.993, \log(0.993) \approx -0.007, loss \approx 0.007. Much smaller. So the loss pushes the model to score winners well above losers.
The \mathbb{E} is just a fancy way of saying “average over the dataset.” In practice it’s literally \frac{1}{N}\sum_{i=1}^{N}. They use \mathbb{E} because it’s more general, but mentally just read it as “average.”
The negation: we want to maximize the log-likelihood (make the data as probable as possible). But every optimization framework (PyTorch, etc.) is set up to minimize a loss. So you slap a minus sign on it. Maximize \log(\text{likelihood}) = minimize -\log(\text{likelihood}). Same thing, just a convention.
Think of each preference pair as a classification problem. For every (x, y_w, y_l), you’re asking: “which response is better?” The label is always y_w (by definition, it’s the one the human picked). So -\log(\sigma(\ldots)) is exactly binary cross-entropy loss when the true label is 1. If you’ve ever trained a logistic regression classifier, it’s the same loss. The reward model is essentially a binary classifier that says “given two responses, which one is better?” and Bradley-Terry via the sigmoid is what connects the reward scores to that binary prediction.
This is the entire machinery that DPO eliminates.
The Cancellation
Bradley-Terry preference modeling only uses the difference in rewards. When you subtract:
Reward for y_1: \;\beta \log \frac{\pi^*(y_1 \mid x)}{\pi_{ref}(y_1 \mid x)} + \beta \log Z(x)
Reward for y_2: \;\beta \log \frac{\pi^*(y_2 \mid x)}{\pi_{ref}(y_2 \mid x)} + \beta \log Z(x)
The \beta \log Z(x) is identical in both. It cancels.
No reward model. No intractable Z(x). Just log-ratios of how the policy diverges from the reference.
The DPO Loss
1 \pi_\theta(y_w \mid x) / \pi_{ref}(y_w \mid x): how much more likely is the winner under the new model vs the original? 2 Same ratio for the loser. 3 Subtract: we want the winner’s ratio to exceed the loser’s. 4 \sigma(\cdot): squash to a probability. 5 \log: log-likelihood. 6 -\mathbb{E}: negate and average.
Training pushes the model to increase the probability of winners and decrease the probability of losers, relative to the reference.
Why Wasn’t DPO Obvious?
If the math is this clean, why did the field spend years on RLHF before someone wrote down DPO? A few reasons.
The RLHF pipeline was built incrementally. Christiano et al. (2017) introduced learning rewards from human preferences. The natural next step was to use those rewards with RL, because that’s what rewards are for. The pipeline worked: train a reward model, then run PPO against it. Each piece made sense on its own, and the combination produced real results. When something works, there’s less pressure to ask whether a simpler path exists.
The key insight in DPO is that you can rearrange the closed-form optimal policy to express reward as a function of the policy itself, then substitute that into Bradley-Terry. This requires noticing that the intractable partition function Z(x) cancels when you only care about reward differences. That cancellation is obvious in hindsight, but it requires you to write down the optimal policy, solve for the reward, and then plug it into the preference model. Most researchers were thinking about the problem in the forward direction: given rewards, find the policy. DPO thinks backward: given the policy, what rewards does it imply?
There’s also a conceptual barrier. RL from human feedback frames alignment as a sequential decision problem. DPO reframes it as supervised learning with a particular loss function. These are different mental models, and switching between them isn’t trivial. The RL framing was dominant in the alignment community, and it took fresh eyes to see that the RL machinery was unnecessary for this specific problem.
Finally, the closed-form solution to the KL-constrained reward maximization was known in the RL literature (it appears in work on maximum entropy RL), but connecting it to preference learning and recognizing the Z(x) cancellation required combining ideas from different subfields. DPO sits at the intersection of preference learning, KL-regularized RL, and supervised fine-tuning. The pieces were all there; someone just had to put them together.
Implementing DPO in Python
The loss function is simple enough to implement from scratch. Here’s a minimal version using PyTorch.
import torch
import torch.nn.functional as F
def dpo_loss(pi_lp_w, pi_lp_l, ref_lp_w, ref_lp_l, beta=0.1):
log_ratio_w = pi_lp_w - ref_lp_w
log_ratio_l = pi_lp_l - ref_lp_l
logits = beta * (log_ratio_w - log_ratio_l)
return -F.logsigmoid(logits).mean()
That’s the entire loss. Four lines of math. The rest is plumbing: computing log-probabilities from a language model, loading preference data, and running a training loop.
Below is a runnable version using only NumPy (so it works in the browser). It trains a toy model on preference pairs and prints how the probability distribution shifts. Click the green play button to run it.
The winner’s probability climbs while the loser drops. That’s DPO doing its job: shifting probability mass toward preferred responses, constrained by the KL penalty against the reference.
A few things to note for real implementations:
- Log-probabilities for a full sequence are the sum of per-token log-probs: \log \pi(y \mid x) = \sum_{t=1}^{T} \log \pi(y_t \mid x, y_{<t})
- The reference model is typically a frozen copy of the model before DPO training
- \beta values between 0.1 and 0.5 are common in practice; lower values allow more aggressive optimization
- Libraries like TRL (Hugging Face) wrap all of this into a
DPOTrainerclass that handles tokenization, batching, and distributed training
RLHF vs DPO
DPO skips the reward model and the RL loop entirely. Simpler, more stable, easier to implement.
Caveats
DPO and RLHF are equivalent in theory, if Bradley-Terry perfectly captures human preferences and you find the global optimum. In practice they can differ: Bradley-Terry is an approximation, the learned reward isn’t the true optimal, and gradient descent on a non-convex landscape doesn’t find the global optimum. There’s also an overfitting risk: if one response always wins in the data, DPO pushes the reward gap toward infinity, driving the loser’s probability to zero.
Feeling generous ? Help me write more blogs like this :)