Preference Alignment: RLHF & DPO Mathematics
Derive the Bradley-Terry preference likelihood, the closed-form optimal policy derivation under KL regularization, the complete algebraic proof of Direct Preference Optimization (DPO), and zero-dependency implementations in Python and Rust.
Pre-trained foundation models are pure text completion engines optimized to predict the next token across internet corpora. Consequently, they mirror internet distributions, generating factually dubious hallucinations, toxic outputs, or evasive responses.
Transforming raw foundation models into helpful, safe assistants requires Preference Alignment. While Classical RLHF requires complex reinforcement learning loops with separate reward models, Direct Preference Optimization (DPO) analytically bypasses reinforcement learning altogether.
Analytically substitutes the optimal policy into the Bradley-Terry model to align LLMs using simple binary cross-entropy.
1. Classical RLHF & The Bradley-Terry Preference Model#
In human preference learning, human annotators are presented with a prompt and two candidate model responses: a winning response and a losing response ().
Under the Bradley-Terry (1952) preference model, the probability that response is preferred over given latent scalar reward model is:
The reward model parameters are trained by minimizing negative log-likelihood:
The PPO Policy Optimization Objective#
Once the reward model is trained, policy is optimized using Proximal Policy Optimization (PPO) against an anchor reference model :
Where penalizes drift away from the original model to prevent reward hacking (exploiting weaknesses in reward model ).
2. Mathematical Derivation of Direct Preference Optimization (DPO)#
Rafailov et al. (2023) observed that the KL-regularized RL objective possesses a closed-form analytical optimum, enabling direct optimization without training a separate reward model or executing PPO loops.
Step 1: The Optimal Policy Solution
Consider the constrained optimization problem over all candidate policies :
Expanding the expectation and KL divergence for a single prompt :
Using calculus of variations subject to constraint , the optimal policy satisfies:
Where partition function normalizes the distribution.
Step 2: Invert the Relation to Isolate the Reward Function
Take the natural logarithm of both sides:
Multiply by and isolate reward :
This remarkable equation proves that the ground-truth reward is uniquely determined by the log-ratio of the optimal policy to the reference policy, plus a prompt-dependent normalization constant.
Step 3: Substitute into the Bradley-Terry Model
Substitute this analytical reward formulation into the Bradley-Terry preference probability:
Evaluate the reward difference :
Notice that the intractable partition function term cancels out completely:
Step 4: Formulate the Final DPO Loss
Applying Maximum Likelihood Estimation over preference dataset :
3. Analysis of the DPO Gradient Update#
To understand how DPO updates network parameters , we compute the gradient with respect to :
The gradient has two intuitive factors:
- Directional Push/Pull: increases the likelihood of preferred response while decreasing the likelihood of rejected response .
- Implicit Weighting: The leading scalar weight scales with how incorrectly the current model scores the pair. If the model already strongly favors , the gradient evaluates close to zero. If the model incorrectly prefers , the gradient scales to its maximum.
4. From-Scratch Implementation: Zero Dependencies#
The following implementations compute the Bradley-Terry Likelihood and the Direct Preference Optimization (DPO) Loss from scratch in pure Python and pure Rust.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566import math from typing import List, Tuple class DPOPrimitives: @staticmethod def _sigmoid(z: float) -> float: if z < -700.0: return 0.0 if z > 700.0: return 1.0 return 1.0 / (1.0 + math.exp(-z)) @classmethod def dpo_loss( cls, policy_chosen_logps: float, # ln pi_theta(y_w | x) policy_rejected_logps: float, # ln pi_theta(y_l | x) reference_chosen_logps: float, # ln pi_ref(y_w | x) reference_rejected_logps: float, # ln pi_ref(y_l | x) beta: float = 0.1 ) -> Tuple[float, float, float]: # Compute log-ratios for winning and losing responses # pi_logratios = ln(pi_theta / pi_ref) chosen_logratio = policy_chosen_logps - reference_chosen_logps rejected_logratio = policy_rejected_logps - reference_rejected_logps # Logit discrepancy: beta * (chosen_logratio - rejected_logratio) logits = beta * (chosen_logratio - rejected_logratio) # Loss = -ln(sigmoid(logits)) # Numerically stable: -ln(1 / (1 + exp(-logits))) = ln(1 + exp(-logits)) if logits > 0: loss = math.log(1.0 + math.exp(-logits)) else: loss = -logits + math.log(1.0 + math.exp(logits)) # Implicit rewards chosen_reward = beta * chosen_logratio rejected_reward = beta * rejected_logratio return loss, chosen_reward, rejected_reward if __name__ == "__main__": # Case 1: Model currently prefers the winning response (low loss) loss1, r_w1, r_l1 = DPOPrimitives.dpo_loss( policy_chosen_logps=-2.5, policy_rejected_logps=-4.0, reference_chosen_logps=-3.0, reference_rejected_logps=-3.0, beta=0.1 ) print("Case 1 (Policy correctly favors winning response):") print(f" DPO Loss: {loss1:.6f}") print(f" Implicit Reward (Winning): {r_w1:.4f}") print(f" Implicit Reward (Losing): {r_l1:.4f}") # Case 2: Model incorrectly prefers the losing response (high loss) loss2, r_w2, r_l2 = DPOPrimitives.dpo_loss( policy_chosen_logps=-4.5, policy_rejected_logps=-1.5, reference_chosen_logps=-3.0, reference_rejected_logps=-3.0, beta=0.1 ) print("\nCase 2 (Policy incorrectly favors losing response):") print(f" DPO Loss: {loss2:.6f}") print(f" Implicit Reward (Winning): {r_w2:.4f}") print(f" Implicit Reward (Losing): {r_l2:.4f}")
5. Active Knowledge Checks#
In the mathematical derivation of Direct Preference Optimization (DPO), what cancellation allows the loss to evaluate without computing the partition function Z(x)?
Why does DPO eliminate the need for an explicit actor-critic reinforcement learning loop (PPO) during LLM alignment?