AI Knowledge Tree

A minimal preference-tuning pass (DPO)

Take the pretrained tiny GPT, build a small preference dataset, and run one DPO step to shift its outputs, a scaled-down version of the alignment pipeline from E4.

The pretrained model from the previous lessons imitates its training distribution; it has no notion of preferred output. Alignment closes that gap. This lesson runs the smallest faithful version of Direct Preference Optimization[1] on the tiny GPT: a reference copy, a handful of preference pairs, and one gradient step on the DPO loss. It is the offline counterpart to the human-feedback fine-tuning that InstructGPT introduced[2].

1. The setup DPO needs

DPO optimizes a policy πθ\pi_\theta against a frozen reference πref\pi_{\text{ref}} using pairs (x,yw,yl)(x, y_w, y_l) where ywy_w is preferred over yly_l for prompt xx. It needs two ingredients beyond the base model:

  • a frozen copy of the pretrained model as the reference, and
  • preference pairs. At toy scale these can be synthetic: pick a property to prefer (say, lines that end in punctuation over lines that do not) and label pairs accordingly.
import copy
import torch
import torch.nn.functional as F

policy = model                       # the pretrained tiny GPT
ref = copy.deepcopy(model)           # frozen reference
for p in ref.parameters():
    p.requires_grad_(False)
ref.eval()

2. Sequence log-probabilities

DPO scores a completion by the policy's total log-probability of its tokens. Run the model, take log-softmax over the vocabulary, and gather the log-prob of each realized next token.

def seq_logprob(m, idx, targets):
    logits, _ = m(idx)                                  # (B,T,V)
    logp = F.log_softmax(logits, dim=-1)
    tok_logp = logp.gather(-1, targets.unsqueeze(-1)).squeeze(-1)  # (B,T)
    return tok_logp.sum(dim=-1)                          # (B,) sum over the sequence

3. The DPO loss

The objective raises the policy's log-prob of the winner relative to the loser, each measured against the reference, inside a logistic loss with temperature β\beta:

LDPO=− E[log⁡σ(β (rθ(x,yw)−rθ(x,yl)))],rθ(x,y)=log⁡πθ(y∣x)πref(y∣x).\mathcal{L}_{\text{DPO}} = -\,\mathbb{E}\Big[\log \sigma\big(\beta\,(r_\theta(x,y_w) - r_\theta(x,y_l))\big)\Big], \quad r_\theta(x,y) = \log\frac{\pi_\theta(y\mid x)}{\pi_{\text{ref}}(y\mid x)}.

The implicit reward rθr_\theta is the log-ratio of policy to reference, which is why DPO needs no separately trained reward model. In code:

def dpo_loss(policy, ref, win, lose, beta=0.1):
    # win, lose: (idx, targets) tensors for preferred and dispreferred completions
    pi_w = seq_logprob(policy, *win)
    pi_l = seq_logprob(policy, *lose)
    with torch.no_grad():
        ref_w = seq_logprob(ref, *win)
        ref_l = seq_logprob(ref, *lose)
    logits = beta * ((pi_w - ref_w) - (pi_l - ref_l))
    return -F.logsigmoid(logits).mean()

opt = torch.optim.AdamW(policy.parameters(), lr=1e-5)
loss = dpo_loss(policy, ref, win_batch, lose_batch, beta=0.1)
opt.zero_grad(set_to_none=True)
loss.backward()
opt.step()

The reference terms are inside torch.no_grad() because only the policy is trained. The β\beta term controls how far the policy may move from the reference: small β\beta keeps it close, large β\beta lets preferences dominate, the same KL-control knob that the RLHF KL penalty provides in the PPO route.

4. What to expect

After a few steps on the synthetic pairs, sample again and compare to the reference: the policy shifts toward the preferred property while staying fluent, because the reference ratio penalizes drifting away from the pretrained distribution. This is the entire shape of preference tuning, minus the scale and the human labels.

Exercises

Exercise 1. At the start of training the policy equals the reference, so the implicit reward rθ(x,y)=log⁡πθ(y∣x)πref(y∣x)r_\theta(x, y) = \log \frac{\pi_\theta(y \mid x)}{\pi_{\text{ref}}(y \mid x)} is zero for every completion. Compute the DPO loss in that initial state, and explain why this value is the natural baseline that any successful preference step must drive the loss below.

Solution

Step 1. When πθ=πref\pi_\theta = \pi_{\text{ref}}, both implicit rewards vanish: rθ(x,yw)=0r_\theta(x, y_w) = 0 and rθ(x,yl)=0r_\theta(x, y_l) = 0. The argument of the logistic is β(0−0)=0\beta(0 - 0) = 0.

Step 2. The loss is −log⁡σ(0)-\log \sigma(0). Since σ(0)=12\sigma(0) = \tfrac12, this is −log⁡12=log⁡2≈0.693-\log \tfrac12 = \log 2 \approx 0.693 nats.

Step 3. This is the baseline: the loss the model starts at before it has expressed any preference. Lowering the loss below log⁡2\log 2 requires σ(β(rw−rl))>12\sigma(\beta(r_w - r_l)) > \tfrac12, which means rw>rlr_w > r_l: the policy must raise the winner's log-ratio above the loser's. A step that fails to separate the two leaves the loss at or above log⁡2\log 2, so log⁡2\log 2 is the threshold that marks the onset of actual preference learning.

Exercise 2 (code). Implement the DPO loss in numpy from sequence log-probabilities (skip the network and feed the log-probs directly). Verify the log⁡2\log 2 baseline from Exercise 1, and confirm that increasing the winner's policy log-prob while holding everything else fixed lowers the loss.

Solution
python · numpy
import numpy as np

def logsigmoid(z):
    # numerically stable log-sigmoid
    return -np.logaddexp(0.0, -z)

def dpo_loss(pi_w, pi_l, ref_w, ref_l, beta=0.1):
    r_w = pi_w - ref_w          # implicit reward for the winner
    r_l = pi_l - ref_l          # implicit reward for the loser
    z = beta * (r_w - r_l)
    return -logsigmoid(z).mean()

# At init the policy equals the reference: all log-ratios are zero.
ref_w = np.array([-3.0, -4.0])
ref_l = np.array([-5.0, -6.0])
pi_w0, pi_l0 = ref_w.copy(), ref_l.copy()
base = dpo_loss(pi_w0, pi_l0, ref_w, ref_l, beta=0.1)
assert np.isclose(base, np.log(2.0))

# Raise the winner's policy log-prob; the loss must drop below the baseline.
pi_w1 = ref_w + 2.0             # policy now prefers the winner
better = dpo_loss(pi_w1, pi_l0, ref_w, ref_l, beta=0.1)
assert better < base

print("baseline loss:", round(float(base), 4), "(log 2)")
print("after preferring winner:", round(float(better), 4))

The loss starts exactly at log⁡2\log 2 and falls once the policy assigns the winner a higher log-ratio than the loser, which is the separation DPO optimizes for.

Exercise 3 (code). The β\beta term is the KL-control knob: small β\beta keeps the policy near the reference, large β\beta lets the preference dominate. Show numerically that for a fixed reward gap rw−rl>0r_w - r_l > 0, the loss decreases as β\beta grows, and explain what that means for how far the policy is pushed.

Solution
python · numpy
import numpy as np

def logsigmoid(z):
    return -np.logaddexp(0.0, -z)

# Fix a positive reward gap (winner already favored by the implicit reward).
gap = 1.5                       # r_w - r_l
betas = np.array([0.05, 0.1, 0.5, 1.0])
losses = np.array([-logsigmoid(b * gap) for b in betas])

# Larger beta -> smaller loss for a fixed positive gap.
assert np.all(np.diff(losses) < 0)

print("betas:  ", betas)
print("losses: ", np.round(losses, 4))

For a fixed positive reward gap the loss is monotonically decreasing in β\beta, so a larger β\beta rewards the same separation more strongly. In training this means a large β\beta produces steeper gradients toward the preferred completion and lets the policy drift further from the reference, while a small β\beta tolerates only a gentle move, the same KL-control tradeoff the PPO route enforces with an explicit penalty.

The model has now been pretrained, modernized, and aligned. The final lesson closes the loop by re-reading the original Transformer paper against the code written across this capstone.


References

  1. [1]Rafailov, R., et al. (2023). "Direct preference optimization (DPO)". NeurIPS. arXiv:2305.18290.↩
  2. [2]Ouyang, L., et al. (2022). "Training language models to follow instructions with human feedback (InstructGPT)". NeurIPS. arXiv:2203.02155.↩