The Big Idea
We have an image generator and lots of pairs of images for the same caption: one we like 👍 (say, a real photo) and one we don't 👎 (say, an image with a plastic, “AI-looking” finish). DPO nudges the model to get better at the 👍 image and worse at the 👎 image, always measured against a frozen copy of where it started.
The plan. We follow the math exactly as a real training loop computes it, one small idea per section:
0 Notation: Meet the Cast
Only six symbols. Every equation on this page is built from these.
Caption
The text prompt. Both images in a pair share the same caption.
Winner
The preferred image (e.g. a real photo), stored as VAE latents.
Loser
The rejected image.
Student
The model being trained. It starts as a copy of the base model.
Reference
A frozen copy of that same base model (often called the “teacher”). It is never updated.
Beta
One number that sets how hard, and for how long, DPO pushes. Section 6 is all about it.
1 The Per-Image Diffusion Loss
Before any DPO, we need one number per image: how well can the model clean up a noisy version of it? This is the ordinary training loss of a rectified-flow (flow-matching) image model.
Pick a time \(t\in(0,1)\) and Gaussian noise \(\varepsilon\sim\mathcal N(0,I)\). Mix them in a straight line, ask the model for the velocity \(v\), and score it with the mean squared error. Lower \(\ell\) means the model “understands” this image better.
Slide t to move between the clean image (t = 0) and pure noise (t = 1). The model error slider plays the role of a better or worse model: a perfect model (0) gives a black error map and ℓ = 0. In real training, images are VAE latents rather than pixels, but the arithmetic is identical. This is the same straight-line idea as the Flow Matching explainer, with time running the other way.
🎲 Which t gets picked?
\(t\) is not uniform. It's drawn from a logit-normal(0, 1) distribution (\(t = \sigma(n)\) with \(n\sim\mathcal N(0,1)\)), which favors the middle, where denoising is hardest and most informative. It is then shifted toward noisier \(t\) by a factor that depends on resolution, because bigger images need more noise to hide their content.
Each falling dot is one sampled \(t\). The curve is the exact density. A common form of the shift is \(t' = \dfrac{s\,t}{1 + (s-1)\,t}\) (used in Stable Diffusion 3). At \(s = 1\) nothing changes; larger \(s\) slides the mass toward \(t = 1\), i.e. more noise.
2 Shared Noise: Four Numbers per Pair
For each pair, draw one \(t\) and one \(\varepsilon\), and reuse them for both the winner and the loser. Then run the frozen reference on exactly the same inputs, with no gradients. That gives four losses per pair. Step through it:
| 👍 winner \(x^w\) | 👎 loser \(x^l\) | |
|---|---|---|
| 🧠 student \(\theta\) (trainable) | \(L^\theta_w = 0.30\) | \(L^\theta_l = 0.50\) |
| 🧊 reference (frozen, no gradients) | \(L^{\text{ref}}_w = 0.40\) | \(L^{\text{ref}}_l = 0.40\) |
🎲 Why share the noise?
A denoising loss depends heavily on luck: which \(t\) and which \(\varepsilon\) were drawn.
If the winner and the loser got different noise, comparing their losses would mostly compare
dice rolls. With the same \((t,\varepsilon)\) for both images and for both models,
every difference we compute next reflects the models, not the noise.
(Training logs often report winner_loss and loser_loss: these are
the batch averages of \(L^\theta_w\) and \(L^\theta_l\).)
3 Reward and Margin
Compare the student with the reference on each image, then compare the two images:
\(r < 0\) means the student fits that image better than the reference does. The goal: fit the winner better (\(r_w\downarrow\)) and the loser worse (\(r_l\uparrow\)), so \(\Delta\) becomes strongly negative.
4 The Loss
One line turns the margin into something to minimize:
\(\sigma(z) = \frac{1}{1+e^{-z}}\) is the sigmoid, which squashes any number into (0, 1).
🧮 A friendlier way to write it: softplus
Since \(\sigma(-z) = \frac{1}{1+e^{z}}\), taking \(-\log\) gives
Softplus is a smooth version of \(\max(0, z)\): almost 0 when \(z\) is very negative, almost \(z\) when it's very positive. So the loss is near 0 once \(\beta\Delta\) is well below 0, and it grows when the margin goes the wrong way.
🧪 Every run starts at 0.693
At step 0 the student is the reference, so every \(r = 0\), \(\Delta = 0\), and \(\mathcal L = \log(1 + e^0) = \ln 2 \approx 0.693\). If your first logged DPO loss isn't 0.693, your reference is not the checkpoint you started from.
🧊 β = 0 means no learning
With \(\beta = 0\) the loss is the constant \(\ln 2\) no matter what \(\Delta\) is: a flat curve, zero gradient. Drag β to 0 in the plot above to see it flatten.
5 Where This Comes From (Diffusion-DPO)
DPO was first written for language models, which can compute exact log-probabilities. Image diffusion models can't, but their training loss stands in for one. Step through the substitution:
Compare how much more likely the student makes the winner vs. the loser, relative to the reference.
The evidence lower bound (ELBO) says: log-likelihood ≈ minus a constant times the denoising loss. Lower loss means more likely.
The “const” is the same for both models, so it cancels. What's left is exactly our reward \(r\) from section 3.
In code it's one line: -logsigmoid(-beta * reward_diff). This is Diffusion-DPO (Wallace et al., 2023).
Optional: why does language-model DPO (step ①) look like that? →
A model of “A beats B” (Bradley–Terry)
Give every output a hidden score \(r(y)\) and assume people prefer \(y_w\) over \(y_l\) with probability
\[P(y_w \succ y_l) = \sigma\big(r(y_w) - r(y_l)\big)\]The goal: high score, but stay near the reference
\[\max_{\pi}\; \mathbb{E}_{y\sim\pi}\big[r(y)\big] - \beta_0\,\mathrm{KL}\big(\pi\,\|\,\pi_{\text{ref}}\big)\]It has an exact solution
Rewrite the objective as \(-\beta_0\,\mathrm{KL}(\pi\,\|\,\pi^*) + \beta_0\log Z\). A KL divergence is smallest (zero) when the two distributions match, so the best model is
\[\pi^*(y) = \frac{1}{Z}\,\pi_{\text{ref}}(y)\,e^{\,r(y)/\beta_0},\qquad Z = \sum_y \pi_{\text{ref}}(y)\,e^{\,r(y)/\beta_0}\]Flip it: the reward hides inside the model
\[r(y) = \beta_0\log\frac{\pi^*(y)}{\pi_{\text{ref}}(y)} + \beta_0\log Z\]Plug into Bradley–Terry: \(Z\) cancels
\[P(y_w\succ y_l) = \sigma\Big(\beta_0\log\frac{\pi^*(y_w)}{\pi_{\text{ref}}(y_w)} - \beta_0\log\frac{\pi^*(y_l)}{\pi_{\text{ref}}(y_l)}\Big)\]Replace the unknown \(\pi^*\) by the student \(\pi_\theta\) and minimize the negative log-likelihood of the observed preferences. That's exactly step ①. No separate reward model is needed.
6 The Gradient Explains Everything About β
The reference losses are constants (no gradients flow through the frozen model), so \(\nabla\Delta = \nabla L^\theta_w - \nabla L^\theta_l\). With the chain rule and \(\frac{d}{dz}\operatorname{softplus}(z) = \sigma(z)\):
Gradient descent moves against this: the winner's loss is pulled down and the loser's loss is pushed up, both with strength \(w\).
🚀 The push starts at β/2
At step 0, \(\Delta = 0\) and \(\sigma(0) = \tfrac12\), so \(w = \beta/2\).
🔌 The push switches itself off
As \(\Delta\) goes negative, \(\sigma(\beta\Delta)\to 0\). Once \(\beta\Delta\approx -5\), \(\sigma \approx 0.007\) and the push is essentially off. That happens at \(|\Delta| \approx 5/\beta\).
What about gradient clipping? Clipping (e.g. to a max norm of 1.0) limits how big each step is, not which direction it goes. It slows a runaway push but doesn't stop it.
7 One Training Step, Animated
Everything above, in the order the computer actually does it. The highlighted step and the matching line of code move together.
x_w, x_l = vae.encode(real), vae.encode(neg)t, eps = sample_t(), randn_like(x_w) # shared!L_w = loss(student, x_w, c, t, eps)L_l = loss(student, x_l, c, t, eps)with torch.no_grad(): R_w = loss(reference, x_w, c, t, eps) R_l = loss(reference, x_l, c, t, eps)delta = (L_w - R_w) - (L_l - R_l)dpo = F.softplus(beta * delta).mean()# = -logsigmoid(-beta * delta)dpo.backward()clip_grad_norm_(student.parameters(), 1.0)optimizer.step(); optimizer.zero_grad()
🎛️ Same equation, three knobs
Every DPO variant you'll meet uses this exact equation. Only three things change:
🏁 The starting checkpoint
The student starts here, and the frozen reference is a copy of it.
👎 Where \(x^l\) comes from
Which images serve as losers: another model's outputs, an older checkpoint's, or the model's own.
🎚️ β
How hard and how long the push lasts (section 6).
Sanity Checks
🧪 First loss = 0.693?
If not, the reference and the starting checkpoint differ, so Δ isn't 0 at step 0. Use the same checkpoint for both.
🎲 Same t and ε everywhere?
Winner, loser, student and reference must all see identical \((t,\varepsilon)\) within a pair. Otherwise Δ is mostly noise.
📉 Watch winner and loser losses separately
The DPO loss can drop just because the loser's loss shoots up. Check that winner_loss isn't rising too, and always look at generated images.
🎯 Pairs teach whatever differs
If 👍 images are also brighter or bigger, the model learns “brighter” or “bigger”. Keep the main difference the one you care about.
Quick Recap: Flip the Cards
Click any card to reveal a plain-English definition.
Want to go deeper?
- Rafailov et al., 2023: Direct Preference Optimization: Your Language Model is Secretly a Reward Model
- Wallace et al., 2023: Diffusion Model Alignment Using Direct Preference Optimization
- Esser et al., 2024: Scaling Rectified Flow Transformers for High-Resolution Image Synthesis (logit-normal t sampling and the resolution shift)
- Companion explainer: Flow Matching