ML Grind

Direct Preference Optimization (DPO) - Reinforcement Learning

Implement Direct Preference Optimization (DPO) in Python with NumPy. hard exercise with examples, hints, and instant feedback. No setup or signup required.

Direct Preference Optimization (DPO)

What you'll build

The DPO loss: train the policy directly on preference pairs, with no reward model and no RL loop.

Background

RLHF maximizes reward with a KL penalty to a reference model. DPO (Rafailov et al., 2023) shows that the optimum of that problem satisfies r(x, y) = β·log(π_θ(y|x) / π_ref(y|x)) + const. Substituting this implicit reward into the Bradley–Terry loss (see RLHF Reward Model Loss) gives a plain classification loss on log-probabilities. It is simpler and more stable than PPO-RLHF, and widely used for open-weight chat models.

Formula

For each pair, with sequence log-probs (the sum of the response's token log-probs):

Δ_chosen   = log π_θ(y_w|x) - log π_ref(y_w|x)
Δ_rejected = log π_θ(y_l|x) - log π_ref(y_l|x)
L_DPO      = -mean[ log σ( β · (Δ_chosen - Δ_rejected) ) ]
  • π_θ: policy being trained; π_ref: frozen reference (usually the SFT model)
  • β: strength of the implicit KL constraint (typically 0.1 to 0.5); larger β keeps π_θ closer to π_ref

Inputs

Four (N,) arrays of sequence log-probs (≤ 0): logp_chosen, logp_rejected under π_θ, and ref_logp_chosen, ref_logp_rejected under π_ref; and beta (float > 0).

Expected return format

A Python float: the mean loss, rounded to 3 decimals.

Edge cases and conventions

  • At initialization π_θ = π_ref, so every Δ is 0 and the loss is log 2 ≈ 0.693.
  • Use a stable log-sigmoid (-np.logaddexp(0, -z)); no eps inside the log.

Constraints

NumPy only.

Follow-up

  • DPO can lower the likelihood of both responses as long as the rejected one drops faster. Why can that hurt, and what do variants such as IPO or adding an SFT term do?
  • Which quantity would you log as "reward accuracy" during DPO training?

Examples

Input: logp_chosen = [-1], logp_rejected = [-2], ref_logp_chosen = [-1.5], ref_logp_rejected = [-1.5], beta = 0.1
Output: 0.644

Δ_chosen = 0.5, Δ_rejected = -0.5, so -log σ(0.1·1.0) = 0.644, just below log 2

Input: logp_chosen = [-3, -2], logp_rejected = [-1, -1], ref logps all -2, beta = 0.5
Output: 1.144

The policy prefers the rejected responses (z = -1 and -0.5), so the loss is above log 2

Practice in Python

import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def dpo_loss(logp_chosen: np.ndarray, logp_rejected: np.ndarray,
             ref_logp_chosen: np.ndarray, ref_logp_rejected: np.ndarray,
             beta: float) -> float:
    """
    Compute the Direct Preference Optimization (DPO) loss.

    Args:
        logp_chosen: (N,) log pi_theta(y_w|x), sequence log-probs
        logp_rejected: (N,) log pi_theta(y_l|x)
        ref_logp_chosen: (N,) log pi_ref(y_w|x)
        ref_logp_rejected: (N,) log pi_ref(y_l|x)
        beta: KL-strength / temperature parameter

    Returns:
        mean of -log sigmoid(beta * (delta_chosen - delta_rejected)) as a float,
        rounded to 3 decimals
    """
    logp_chosen = np.array(logp_chosen, dtype=float)
    logp_rejected = np.array(logp_rejected, dtype=float)
    ref_logp_chosen = np.array(ref_logp_chosen, dtype=float)
    ref_logp_rejected = np.array(ref_logp_rejected, dtype=float)

    # Your code here
    pass

The interactive editor loads with Python and NumPy. Run your implementation against 6 tests, or use the progressive hints.

More exercises in this section →