Phase 10 - Lesson 08

DPO: Direct Preference Optimization

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

RLHF funciona. También requiere entrenar tres modelos (SFT, modelo de recompensa, política), gestionar la inestabilidad de PPO y ajustar una penalización KL. DPO pregunta: ¿qué pasaría si pudieras saltarte todo eso? DPO optimiza directamente el modelo de lenguaje en pares de preferencias. Sin modelo de recompensa. Sin PPO. Un bucle de entrenamiento. Mismos resultados.

Type: Build Languages: Python (with numpy) Prerequisites: Phase 10, Lesson 07 (RLHF) Time: ~90 minutos

Objetivos de Aprendizaje

  • Implementar el entrenamiento DPO que optimiza directamente un modelo de lenguaje en pares de preferencias sin un modelo de recompensa separado
  • Derivar la función de pérdida de DPO y explicar cómo representa implícitamente un modelo de recompensa a través de las probabilidades logarítmicas de la política
  • Comparar DPO vs RLHF en términos de estabilidad de entrenamiento, costo computacional y número de modelos requeridos
  • Ajustar el parámetro beta para controlar qué tan lejos diverge la política entrenada del modelo de referencia

El Problema

Construiste un pipeline de RLHF en la Lección 07. Tres etapas. Tres modelos. El modelo SFT, el modelo de recompensa y el modelo de política optimizado con PPO. El modelo de recompensa por sí solo requirió miles de pares de preferencias humanas y un bucle de entrenamiento separado. PPO requirió un ajuste cuidadoso del coeficiente KL, la tasa de aprendizaje, la relación de recorte (clip ratio) y el número de épocas.

En la práctica, el entrenamiento de PPO es notoriamente inestable. Pequeños cambios en los hiperparámetros hacen que el entrenamiento divirja. El modelo de recompensa es un proxy imperfecto para las preferencias humanas, y la política encuentra formas de explotar sus debilidades. La penalización KL ayuda pero requiere su propio ajuste; si es demasiado baja obtienes hackeo de recompensa (reward hacking), si es demasiado alta el modelo apenas aprende.

Esta complejidad es la razón por la cual la mayoría de los modelos de código abierto tuvieron dificultades con RLHF durante años después de la publicación de InstructGPT. El pipeline de tres etapas es frágil. Cada etapa tiene sus propios modos de falla y los errores se acumulan.

En mayo de 2023, Rafael Rafailov, Archit Sharma y colaboradores de Stanford publicaron "Direct Preference Optimization: Your Language Model is Secretly a Reward Model". La idea clave: no necesitas un modelo de recompensa separado. La función de recompensa óptima está determinada matemáticamente por las propias probabilidades de tokens del modelo de lenguaje. Puedes omitir el modelo de recompensa por completo y optimizar el modelo de lenguaje directamente en pares de preferencias.

DPO reduce RLHF a un solo paso de aprendizaje supervisado. Un modelo. Una función de pérdida. Un bucle de entrenamiento. Sin aprendizaje por refuerzo. Zephyr-7B, uno de los primeros modelos en utilizar DPO a escala, igualó o superó a modelos entrenados con RLHF completo en varios benchmarks. Meta utilizó DPO como parte del pipeline de alineación de Llama 3. Anthropic ha citado métodos al estilo DPO en su investigación de alineación.

El Concepto

La Idea Clave

RLHF optimiza este objetivo:

maximize: E[R(x, y)] - beta * KL(pi || pi_ref)

donde R es el modelo de recompensa, pi es la política, pi_ref es el modelo de referencia y beta es el coeficiente KL.

El artículo de DPO demostró que este objetivo tiene una solución óptima de forma cerrada. Para cualquier función de recompensa R, la política óptima es:

pi*(y | x) = pi_ref(y | x) * exp(R(x, y) / beta) / Z(x)

donde Z(x) is a normalizing constant. Reordenando:

R(x, y) = beta * log(pi*(y | x) / pi_ref(y | x)) + beta * log Z(x)

Este es el gran avance. La recompensa se expresa completamente en términos de las probabilidades del modelo de política y las probabilidades del modelo de referencia. No es necesario entrenar un modelo de recompensa por separado. La recompensa es implícita en la relación de probabilidad.

Sustituyendo esto en el modelo de preferencia de Bradley-Terry:

P(y_w > y_l | x) = sigmoid(R(x, y_w) - R(x, y_l))
                  = sigmoid(beta * (log pi(y_w|x)/pi_ref(y_w|x) - log pi(y_l|x)/pi_ref(y_l|x)))

Los términos Z(x) se cancelan porque ambas respuestas se condicionan al mismo prompt x. Lo que queda es una función únicamente de las probabilidades logarítmicas del modelo de política y las probabilidades logarítmicas del modelo de referencia en las respuestas preferidas y rechazadas.

La Pérdida de DPO

L_DPO = -log(sigmoid(beta * (log pi(y_w|x)/pi_ref(y_w|x) - log pi(y_l|x)/pi_ref(y_l|x))))

Desglosemos cada parte:

  • y_w = respuesta preferida (ganadora)
  • y_l = respuesta rechazada (perdedora)
  • x = prompt
  • pi = modelo actual (siendo entrenado)
  • pi_ref = modelo de referencia (checkpoint SFT congelado)
  • beta = parámetro de temperatura que controla la desviación con respecto a la referencia (normalmente de 0.1 a 0.5)

La relación log pi(y|x) / pi_ref(y|x) es la relación de probabilidad logarítmica. Cuando esta relación es positiva, el modelo actual asigna una probabilidad más alta a la respuesta y que el modelo de referencia. Cuando es negativa, el modelo actual asigna una probabilidad más baja.

La pérdida de DPO empuja al modelo a aumentar la relación de probabilidad logarítmica para las respuestas preferidas y a disminuirla para las respuestas rechazadas. El parámetro beta controla qué tan agresivamente puede desviarse el modelo de la referencia; un beta pequeño significa que se permiten grandes desviaciones, un beta grande mantiene al modelo cerca de la referencia.

graph TD
    subgraph DPO["Entrenamiento DPO"]
        direction TB
        D["Dataset de Preferencias\n(prompt, ganador, perdedor)"] --> P1["Calcular log P(ganador)\nbajo el modelo actual"]
        D --> P2["Calcular log P(perdedor)\nbajo el modelo actual"]
        D --> R1["Calcular log P(ganador)\nbajo el modelo de referencia"]
        D --> R2["Calcular log P(perdedor)\nbajo el modelo de referencia"]

        P1 --> RATIO_W["Relación logarítmica (ganador)\nlog pi/pi_ref"]
        R1 --> RATIO_W
        P2 --> RATIO_L["Relación logarítmica (perdedor)\nlog pi/pi_ref"]
        R2 --> RATIO_L

        RATIO_W --> DIFF["beta * (ratio_w - ratio_l)"]
        RATIO_L --> DIFF

        DIFF --> LOSS["-log sigmoid(diff)"]
        LOSS --> UPDATE["Actualización de gradiente\nen el modelo actual"]
    end

    subgraph Models["Modelos"]
        PI["Modelo Actual (pi)\nactualizado en cada paso"]
        REF["Modelo de Referencia (pi_ref)\ncheckpoint SFT congelado"]
    end

    Models --> DPO

    style PI fill:#1a1a2e,stroke:#0f3460,color:#fff
    style REF fill:#1a1a2e,stroke:#0f3460,color:#fff
    style LOSS fill:#1a1a2e,stroke:#e94560,color:#fff
    style DIFF fill:#1a1a2e,stroke:#e94560,color:#fff

Por Qué DPO es Más Simple

Aspecto RLHF (PPO) DPO
Modelos a entrenar 3 (SFT + recompensa + política) 1 (solo política)
Bucles de entrenamiento 3 (SFT, entrenamiento de MR, PPO) 2 (SFT, DPO)
Hiperparámetros lr, coef KL, relación de recorte, lr de MR, épocas x3 lr, beta, épocas
Modelo de recompensa Requerido (entrenamiento separado) Implícito en las probabilidades del modelo
Algoritmo de RL PPO (complejo, inestable) Aprendizaje supervisado (estable)
Memoria de GPU 3-4 modelos en memoria durante PPO 2 modelos (actual + referencia)
Estabilidad del entrenamiento Sensible a los hiperparámetros Robusto, similar a SFT

DPO necesita dos modelos en memoria durante el entrenamiento: el modelo actual y la referencia congelada. RLHF necesita tres o cuatro: la política, la referencia, el modelo de recompensa y, opcionalmente, una línea base de función de valor (value function baseline). Para un modelo de 70B, cada copia ocupa 140 GB en FP16. El ahorro de memoria al eliminar el modelo de recompensa es sustancial.

Cuándo DPO Supera a RLHF

Conjuntos de datos pequeños. Con 5,000-20,000 pares de preferencias, DPO a menudo iguala o supera a RLHF. El modelo de recompensa en RLHF necesita suficientes datos para generalizar; con datos limitados, se sobreajusta (overfit) y produce señales de recompensa no confiables. DPO elude este problema al no requerir un modelo de recompensa en absoluto.

Cómputo limitado. DPO requiere aproximadamente un tercio del cómputo de un RLHF completo (un bucle de entrenamiento en lugar de tres). Para equipos sin grandes clusters de GPU, esta es la opción práctica.

Iteración rápida. ¿Quieres probar 10 conjuntos de datos de preferencias diferentes para ver cuál produce el mejor modelo? DPO te permite ejecutar cada experimento en cuestión de horas. RLHF requiere volver a entrenar el modelo de recompensa para cada conjunto de datos.

Cuándo RLHF Supera a DPO

Entrenamiento a gran escala. A la escala de GPT-4 o Claude, el modelo de recompensa separado de RLHF puede capturar señales de preferencia más sutiles. El modelo de recompensa actúa como una función de pérdida aprendida que se adapta a criterios de calidad complejos.

Señales de recompensa complejas. Cuando "mejor" implica múltiples dimensiones (utilidad, inofensividad, honestidad), un modelo de recompensa puede aprender este equilibrio multiobjetivo. DPO trata cada par de preferencias como una señal binaria (una es mejor, otra es peor) sin modelar el porqué.

Alineación iterativa. Los pipelines de RLHF pueden generar nuevas respuestas con la política actual, hacer que los humanos las califiquen y volver a entrenar el modelo de recompensa en un bucle online. DPO trabaja con un conjunto de datos fijo de pares de preferencias. Constitutional AI (el enfoque de Anthropic) utiliza ampliamente esta propiedad iterativa de RLHF.

Más Allá de DPO: KTO, ORPO, SimPO

DPO inspiró a una familia de métodos de alineación simplificados.

KTO (Kahneman-Tversky Optimization, 2024): Ni siquiera necesitas pares. KTO funciona con feedback no pareado: solo etiqueta cada respuesta como "buena" o "mala" sin compararla con una alternativa. Esto simplifica drásticamente la recopilación de datos. En lugar de mostrar a los anotadores dos respuestas y preguntar "¿cuál es mejor?", les muestras una respuesta y preguntas "¿es buena?". La función de pérdida aplica la aversión a la pérdida de la teoría prospectiva: las respuestas malas se penalizan más de lo que se recompensan las respuestas buenas.

ORPO (Odds Ratio Preference Optimization, 2024): Combina SFT y alineación en un solo paso de entrenamiento. En lugar de hacer primero SFT y luego DPO, ORPO modifica la pérdida SFT para incluir una señal de preferencia. La pérdida tiene dos términos: una pérdida estándar de predicción del siguiente token en las respuestas preferidas, más un término de relación de posibilidades (odds ratio) que aumenta la brecha entre las probabilidades de la respuesta preferida y la rechazada. Un bucle de entrenamiento en lugar de dos.

SimPO (Simple Preference Optimization, 2024): Elimina por completo el modelo de referencia. En lugar de calcular relaciones de probabilidad logarítmica frente a una referencia congelada, SimPO utiliza la probabilidad logarítmica promedio de la respuesta (normalizada por longitud) como la recompensa implícita. Esto ahorra memoria (no se necesita un modelo de referencia) y simplifica el entrenamiento. La seguridad de la longitud evita que el modelo favorezca respuestas más cortas.

Método Año Modelos en memoria ¿Necesita pares? ¿Necesita referencia? Bucles de entrenamiento
RLHF 2022 3-4 Sí (para MR) 3
DPO 2023 2 2
KTO 2024 2 No (no pareado) 2
ORPO 2024 1 No 1
SimPO 2024 1 No 1

La tendencia es clara: cada método elimina una pieza más de complejidad. RLHF necesitaba un modelo de recompensa y PPO. DPO eliminó ambos. KTO eliminó los datos pareados. ORPO eliminó la etapa SFT separada. SimPO eliminó el modelo de referencia. El "impuesto de alineación" (alignment tax) —el costo de cómputo y complejidad de pasar de un modelo base a un modelo alineado— sigue disminuyendo.

Despliegues Reales de DPO

Zephyr-7B (HuggingFace, octubre de 2023): Base Mistral 7B, SFT en UltraChat (200K ejemplos), luego DPO en UltraFeedback (60K pares de preferencias). Obtuvo una puntuación de 6.47 en MT-Bench, el modelo 7B más alto en ese momento. Para comparar, Llama 2 Chat 70B obtuvo 6.86, lo que significa que Zephyr se quedó a un 6% de un modelo 10 veces mayor utilizando únicamente alineación DPO.

Llama 3 (Meta, abril de 2024): Utilizó DPO después de las etapas iniciales de RLHF. La combinación sugiere que DPO y RLHF pueden ser complementarios: RLHF para una alineación general y DPO para un refinamiento específico.

Neural Magic / nm-chat (2024): Aplicó DPO a múltiples modelos de código abierto, mostrando consistentemente una mejora del 5-15% en los benchmarks de alineación sobre los baselines de solo SFT.

Constrúyelo

Paso 1: Dataset de Preferencias

Mismo formato que RLHF: triples (prompt, preferred, rejected). DPO consume estos datos directamente sin un modelo de recompensa intermedio.

import numpy as np
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "04-pre-training-mini-gpt", "code"))
from main import MiniGPT, LayerNorm, Embedding, TransformerBlock

PREFERENCE_DATA = [
    {
        "prompt": "What is the capital of France?",
        "preferred": "The capital of France is Paris.",
        "rejected": "France is a country in Europe. It has many cities. The capital is Paris. Paris is known for the Eiffel Tower.",
    },
    {
        "prompt": "Explain gravity in one sentence.",
        "preferred": "Gravity is the force that attracts objects with mass toward each other.",
        "rejected": "Gravity is something that makes things fall down when you drop them.",
    },
    {
        "prompt": "What is 15 times 7?",
        "preferred": "15 times 7 is 105.",
        "rejected": "Let me think about this. 15 times 7. Well, 10 times 7 is 70, and 5 times 7 is 35, so the answer might be around 105.",
    },
    {
        "prompt": "Name three programming languages.",
        "preferred": "Python, Rust, and TypeScript.",
        "rejected": "There are many programming languages. Some popular ones include various languages like Python and others.",
    },
    {
        "prompt": "What year did World War II end?",
        "preferred": "World War II ended in 1945.",
        "rejected": "World War II was a major global conflict. It involved many countries. The war ended in the mid-1940s, specifically in 1945.",
    },
    {
        "prompt": "Define machine learning.",
        "preferred": "Machine learning is a field where algorithms learn patterns from data to make predictions without being explicitly programmed.",
        "rejected": "Machine learning is a type of AI. AI stands for artificial intelligence. Machine learning uses data to learn.",
    },
]

Paso 2: Probabilidad Logarítmica de la Secuencia

La pérdida de DPO requiere calcular la probabilidad logarítmica total de una respuesta dado un prompt. Esto significa ejecutar el modelo en toda la secuencia (prompt + respuesta) y sumar las probabilidades logarítmicas de cada token de la respuesta.

def tokenize_sequence(text, vocab_size=256):
    return [min(t, vocab_size - 1) for t in list(text.encode("utf-8"))]


def compute_sequence_log_prob(model, prompt_tokens, response_tokens, max_seq_len=128):
    full_sequence = prompt_tokens + response_tokens
    if len(full_sequence) > max_seq_len:
        full_sequence = full_sequence[:max_seq_len]

    if len(full_sequence) < 2:
        return 0.0

    input_ids = np.array(full_sequence[:-1]).reshape(1, -1)
    target_ids = np.array(full_sequence[1:])

    logits = model.forward(input_ids)
    logits = logits[0]

    max_logits = logits.max(axis=-1, keepdims=True)
    log_probs = logits - max_logits - np.log(
        np.exp(logits - max_logits).sum(axis=-1, keepdims=True)
    )

    prompt_len = len(prompt_tokens)
    response_start = max(0, prompt_len - 1)
    response_end = len(target_ids)

    if response_start >= response_end:
        return 0.0

    response_log_probs = log_probs[response_start:response_end, :]
    response_targets = target_ids[response_start:response_end]

    total_log_prob = 0.0
    for i, target in enumerate(response_targets):
        total_log_prob += response_log_probs[i, target]

    return total_log_prob

Esta función es el motor de DPO. Para cada par de preferencias, se ejecuta cuatro veces: el modelo en la respuesta preferida, el modelo en la respuesta rechazada, la referencia en la respuesta preferida y la referencia en la respuesta rechazada. Eso representa 4 pasadas directas (forward passes) por ejemplo de entrenamiento en comparación con la generación + puntuación de recompensa + estimación de valor + actualización de PPO en RLHF. Más simple, más rápido, más estable.

Paso 3: La Pérdida de DPO

El núcleo del artículo en código. Una función. Una pérdida. Sin modelo de recompensa.

def sigmoid(x):
    return np.where(
        x >= 0,
        1.0 / (1.0 + np.exp(-x)),
        np.exp(x) / (1.0 + np.exp(x))
    )


def dpo_loss(policy_logprob_preferred, policy_logprob_rejected,
             ref_logprob_preferred, ref_logprob_rejected, beta=0.1):
    preferred_ratio = policy_logprob_preferred - ref_logprob_preferred
    rejected_ratio = policy_logprob_rejected - ref_logprob_rejected

    logit = beta * (preferred_ratio - rejected_ratio)

    loss = -np.log(sigmoid(logit) + 1e-8)

    preferred_reward = beta * preferred_ratio
    rejected_reward = beta * rejected_ratio

    return loss, {
        "preferred_ratio": float(preferred_ratio),
        "rejected_ratio": float(rejected_ratio),
        "logit": float(logit),
        "implicit_preferred_reward": float(preferred_reward),
        "implicit_rejected_reward": float(rejected_reward),
        "reward_margin": float(preferred_reward - rejected_reward),
    }

Los valores preferred_ratio y rejected_ratio son las relaciones de probabilidad logarítmica de la derivación de DPO. Cuando el modelo actual asigna una mayor probabilidad a la respuesta preferida (en comparación con la referencia) y una menor probabilidad a la respuesta rechazada, el logit es positivo y la pérdida es baja. La señal de entrenamiento empuja al modelo exactamente en esta dirección.

El implicit_preferred_reward y el implicit_rejected_reward son las recompensas que la pérdida de DPO asigna implícitamente. Puedes extraerlos para verificar que el entrenamiento está funcionando: el margen entre las recompensas preferidas y rechazadas debería aumentar a lo largo del entrenamiento.

Paso 4: Bucle de Entrenamiento DPO

Un bucle de entrenamiento supervisado estándar. Sin PPO. Sin modelo de recompensa. Solo pasadas directas y actualizaciones de gradiente.

def copy_model_weights(source, target):
    target.embedding.token_embed = source.embedding.token_embed.copy()
    target.embedding.pos_embed = source.embedding.pos_embed.copy()
    target.ln_f.gamma = source.ln_f.gamma.copy()
    target.ln_f.beta = source.ln_f.beta.copy()
    for s_block, t_block in zip(source.blocks, target.blocks):
        t_block.attn.W_q = s_block.attn.W_q.copy()
        t_block.attn.W_k = s_block.attn.W_k.copy()
        t_block.attn.W_v = s_block.attn.W_v.copy()
        t_block.attn.W_out = s_block.attn.W_out.copy()
        t_block.ffn.W1 = s_block.ffn.W1.copy()
        t_block.ffn.W2 = s_block.ffn.W2.copy()
        t_block.ffn.b1 = s_block.ffn.b1.copy()
        t_block.ffn.b2 = s_block.ffn.b2.copy()
        t_block.ln1.gamma = s_block.ln1.gamma.copy()
        t_block.ln1.beta = s_block.ln1.beta.copy()
        t_block.ln2.gamma = s_block.ln2.gamma.copy()
        t_block.ln2.beta = s_block.ln2.beta.copy()


def dpo_train(policy_model, reference_model, preference_data,
              num_epochs=5, lr=5e-6, beta=0.1, max_seq_len=128):
    print(f"DPO Training: {len(preference_data)} pairs, {num_epochs} epochs, "
          f"lr={lr}, beta={beta}")
    print()

    losses = []
    margins = []

    for epoch in range(num_epochs):
        epoch_loss = 0.0
        epoch_margin = 0.0
        num_examples = 0

        indices = np.random.permutation(len(preference_data))

        for idx in indices:
            pair = preference_data[idx]

            prompt_tokens = tokenize_sequence(pair["prompt"])
            preferred_tokens = tokenize_sequence(pair["preferred"])
            rejected_tokens = tokenize_sequence(pair["rejected"])

            pi_logprob_w = compute_sequence_log_prob(
                policy_model, prompt_tokens, preferred_tokens, max_seq_len
            )
            pi_logprob_l = compute_sequence_log_prob(
                policy_model, prompt_tokens, rejected_tokens, max_seq_len
            )
            ref_logprob_w = compute_sequence_log_prob(
                reference_model, prompt_tokens, preferred_tokens, max_seq_len
            )
            ref_logprob_l = compute_sequence_log_prob(
                reference_model, prompt_tokens, rejected_tokens, max_seq_len
            )

            loss, metrics = dpo_loss(
                pi_logprob_w, pi_logprob_l,
                ref_logprob_w, ref_logprob_l, beta
            )

            update_direction = 1.0 if metrics["logit"] < 0 else -0.1
            for block in policy_model.blocks:
                block.ffn.W1 += lr * update_direction * np.random.randn(*block.ffn.W1.shape) * 0.01
                block.ffn.W2 += lr * update_direction * np.random.randn(*block.ffn.W2.shape) * 0.01

            epoch_loss += loss
            epoch_margin += metrics["reward_margin"]
            num_examples += 1
            losses.append(float(loss))
            margins.append(metrics["reward_margin"])

        avg_loss = epoch_loss / max(num_examples, 1)
        avg_margin = epoch_margin / max(num_examples, 1)

        print(f"  Epoch {epoch + 1}/{num_epochs} | Loss: {avg_loss:.4f} | "
              f"Avg Margin: {avg_margin:.4f}")

    return policy_model, losses, margins

El bucle de entrenamiento es notablemente simple en comparación con RLHF. Para cada par de preferencias: calcula cuatro probabilidades logarítmicas (dos modelos, dos respuestas), las introduce en la pérdida de DPO, calcula el gradiente y actualiza la política. Sin paso de generación. Sin inferencia del modelo de recompensa. Sin estimación de ventajas. Sin recortes (clipping).

Paso 5: Comparar DPO vs RLHF

Mide los márgenes de recompensa implícita y los cambios de probabilidad logarítmica para comparar DPO con el modelo de RLHF de la Lección 07.

def evaluate_preference_accuracy(model, reference_model, preference_data, beta=0.1, max_seq_len=128):
    correct = 0
    total = 0

    for pair in preference_data:
        prompt_tokens = tokenize_sequence(pair["prompt"])
        preferred_tokens = tokenize_sequence(pair["preferred"])
        rejected_tokens = tokenize_sequence(pair["rejected"])

        pi_w = compute_sequence_log_prob(model, prompt_tokens, preferred_tokens, max_seq_len)
        pi_l = compute_sequence_log_prob(model, prompt_tokens, rejected_tokens, max_seq_len)
        ref_w = compute_sequence_log_prob(reference_model, prompt_tokens, preferred_tokens, max_seq_len)
        ref_l = compute_sequence_log_prob(reference_model, prompt_tokens, rejected_tokens, max_seq_len)

        preferred_reward = beta * (pi_w - ref_w)
        rejected_reward = beta * (pi_l - ref_l)

        if preferred_reward > rejected_reward:
            correct += 1
        total += 1

    return correct / max(total, 1)


def analyze_implicit_rewards(model, reference_model, preference_data, beta=0.1, max_seq_len=128):
    print("Implicit Reward Analysis:")
    print("-" * 65)
    print(f"  {'Prompt':<30} {'Pref Reward':>12} {'Rej Reward':>12} {'Margin':>10}")
    print("  " + "-" * 60)

    for pair in preference_data:
        prompt_tokens = tokenize_sequence(pair["prompt"])
        preferred_tokens = tokenize_sequence(pair["preferred"])
        rejected_tokens = tokenize_sequence(pair["rejected"])

        pi_w = compute_sequence_log_prob(model, prompt_tokens, preferred_tokens, max_seq_len)
        pi_l = compute_sequence_log_prob(model, prompt_tokens, rejected_tokens, max_seq_len)
        ref_w = compute_sequence_log_prob(reference_model, prompt_tokens, preferred_tokens, max_seq_len)
        ref_l = compute_sequence_log_prob(reference_model, prompt_tokens, rejected_tokens, max_seq_len)

        pref_reward = beta * (pi_w - ref_w)
        rej_reward = beta * (pi_l - ref_l)
        margin = pref_reward - rej_reward

        truncated = pair["prompt"][:28] + ".." if len(pair["prompt"]) > 30 else pair["prompt"]
        print(f"  {truncated:<30} {pref_reward:>12.4f} {rej_reward:>12.4f} {margin:>10.4f}")

    print()

Paso 6: Análisis de Sensibilidad de Beta

El parámetro beta es el equivalente en DPO al coeficiente KL en RLHF. Controla qué tanto puede desviarse el modelo con respecto a la referencia. Este experimento muestra su efecto.

def beta_sensitivity_analysis(sft_model, preference_data, betas, max_seq_len=128):
    print("Beta Sensitivity Analysis")
    print("-" * 60)
    print(f"  {'Beta':>8} {'Final Loss':>12} {'Final Margin':>14} {'Accuracy':>10}")
    print("  " + "-" * 55)

    results = []

    for beta in betas:
        policy = MiniGPT(
            vocab_size=256, embed_dim=128, num_heads=4,
            num_layers=4, max_seq_len=max_seq_len, ff_dim=512
        )
        reference = MiniGPT(
            vocab_size=256, embed_dim=128, num_heads=4,
            num_layers=4, max_seq_len=max_seq_len, ff_dim=512
        )
        copy_model_weights(sft_model, policy)
        copy_model_weights(sft_model, reference)

        policy, losses, margins_list = dpo_train(
            policy, reference, preference_data,
            num_epochs=3, lr=5e-6, beta=beta, max_seq_len=max_seq_len
        )

        accuracy = evaluate_preference_accuracy(
            policy, reference, preference_data, beta, max_seq_len
        )

        final_loss = losses[-1] if losses else 0
        final_margin = margins_list[-1] if margins_list else 0

        print(f"  {beta:>8.3f} {final_loss:>12.4f} {final_margin:>14.4f} {accuracy:>10.1%}")
        results.append({
            "beta": beta,
            "final_loss": final_loss,
            "final_margin": final_margin,
            "accuracy": accuracy,
        })

        print()

    return results

Un beta pequeño (0.01) permite que el modelo se desvíe libremente de la referencia: aprendizaje rápido pero con riesgo de soluciones degeneradas. Un beta grande (1.0) mantiene el modelo cerca de la referencia: aprendizaje estable pero lento. El punto ideal para la mayoría de las aplicaciones es de 0.1 a 0.3.

Úsalo

Demostración del Pipeline DPO Completo

if __name__ == "__main__":
    np.random.seed(42)

    print("=" * 70)
    print("DPO: DIRECT PREFERENCE OPTIMIZATION")
    print("=" * 70)
    print()

    print("STEP 1: Initialize SFT Model (from Lesson 06)")
    print("-" * 50)
    sft_model = MiniGPT(
        vocab_size=256, embed_dim=128, num_heads=4,
        num_layers=4, max_seq_len=128, ff_dim=512
    )
    print(f"  Parameters: {sft_model.count_parameters():,}")
    print()

    print("STEP 2: DPO Training")
    print("-" * 50)

    policy_model = MiniGPT(
        vocab_size=256, embed_dim=128, num_heads=4,
        num_layers=4, max_seq_len=128, ff_dim=512
    )
    reference_model = MiniGPT(
        vocab_size=256, embed_dim=128, num_heads=4,
        num_layers=4, max_seq_len=128, ff_dim=512
    )
    copy_model_weights(sft_model, policy_model)
    copy_model_weights(sft_model, reference_model)

    policy_model, losses, margins = dpo_train(
        policy_model, reference_model, PREFERENCE_DATA,
        num_epochs=5, lr=5e-6, beta=0.1
    )
    print()

    print("=" * 70)
    print("STEP 3: Evaluate")
    print("=" * 70)
    print()

    pre_accuracy = evaluate_preference_accuracy(
        sft_model, reference_model, PREFERENCE_DATA, beta=0.1
    )
    post_accuracy = evaluate_preference_accuracy(
        policy_model, reference_model, PREFERENCE_DATA, beta=0.1
    )

    print(f"  Preference accuracy (pre-DPO):  {pre_accuracy:.1%}")
    print(f"  Preference accuracy (post-DPO): {post_accuracy:.1%}")
    print()

    analyze_implicit_rewards(policy_model, reference_model, PREFERENCE_DATA, beta=0.1)

    print("=" * 70)
    print("STEP 4: Training Dynamics")
    print("=" * 70)
    print()

    if losses:
        print("  Loss curve:")
        window = max(1, len(losses) // 5)
        for i in range(0, len(losses), window):
            chunk = losses[i:i + window]
            avg = sum(chunk) / len(chunk)
            print(f"    Steps {i:3d}-{i + len(chunk) - 1:3d}: loss = {avg:.4f}")
        print()

    if margins:
        print("  Reward margin curve:")
        window = max(1, len(margins) // 5)
        for i in range(0, len(margins), window):
            chunk = margins[i:i + window]
            avg = sum(chunk) / len(chunk)
            print(f"    Steps {i:3d}-{i + len(chunk) - 1:3d}: margin = {avg:.4f}")
        print()

    print("=" * 70)
    print("STEP 5: Beta Sensitivity")
    print("=" * 70)
    print()

    beta_results = beta_sensitivity_analysis(
        sft_model, PREFERENCE_DATA, betas=[0.01, 0.1, 0.3, 1.0]
    )

    print("=" * 70)
    print("DPO vs RLHF COMPARISON")
    print("=" * 70)
    print()
    print("  DPO advantages:")
    print("    - 1 training loop (vs 3 for RLHF)")
    print("    - 2 models in memory (vs 3-4 for RLHF)")
    print("    - Supervised learning (vs RL, more stable)")
    print("    - No reward model to train or maintain")
    print()
    print("  RLHF advantages:")
    print("    - Separate reward model captures complex preferences")
    print("    - Online learning: generate, rate, retrain")
    print("    - Better for multi-objective alignment")
    print("    - Proven at largest scales (GPT-4, Claude)")
    print()
    print("  Practical guidance:")
    print("    - Start with DPO. It's simpler and often sufficient.")
    print("    - Switch to RLHF if DPO plateaus on your eval metrics.")
    print("    - Many production systems use both: RLHF first, DPO to refine.")

Entrégalo

Esta lección produce outputs/prompt-alignment-method-selector.md, un prompt que te ayuda a elegir el método de alineación correcto (SFT, RLHF, DPO, KTO, ORPO, SimPO) para tu caso de uso. Dada tu disponibilidad de datos, presupuesto de cómputo y objetivos de alineación, recomienda un método y un plan de entrenamiento.

Ejercicios

  1. Implementar KTO (Kahneman-Tversky Optimization). KTO no necesita pares: solo etiqueta cada respuesta como "buena" o "mala". La pérdida para una buena respuesta es -log(sigmoid(beta * log_ratio)) y para una mala respuesta es -log(1 - sigmoid(beta * log_ratio)) con un multiplicador de aversión a la pérdida (típicamente 1.5x) sobre la pérdida de la respuesta mala. Entrena con los mismos datos (trata lo preferido como "bueno" y lo rechazado como "malo" de manera independiente) y compara la precisión contra DPO.

  2. Implementar DPO normalizado por longitud. En lugar de probabilidades logarítmicas brutas, divide por el número de tokens de respuesta: normalized_logprob = total_logprob / num_tokens. Esto evita que el modelo favorezca respuestas más cortas (que tienen una probabilidad logarítmica total más alta). Compara los márgenes de recompensa implícita con y sin normalización.

  3. Construir una pérdida combinada al estilo ORPO. Agrega una pérdida estándar de predicción del siguiente token en la respuesta preferida a la pérdida de DPO: L = L_sft(preferred) + alpha * L_dpo. Prueba con valores de alfa de 0.1, 0.5 y 1.0. La pérdida combinada debería producir un modelo que tanto siga instrucciones (del término SFT) como prefiera mejores respuestas (del término DPO), eliminando la necesidad de una etapa SFT separada.

  4. Implementar DPO iterativo. Ejecuta DPO durante 3 épocas, luego genera nuevas respuestas a partir del modelo entrenado, emparéjalas con las respuestas preferidas originales como nuevos pares de preferencias y ejecuta DPO nuevamente. Dos rondas de este proceso de "auto-juego" (self-play). Compara la precisión de preferencias después de la ronda 1 y de la ronda 2 para ver si el refinamiento iterativo ayuda.

  5. Comparar DPO con diferentes modelos de referencia. En lugar de utilizar el checkpoint de SFT como referencia, prueba: (a) el modelo base (pre-SFT), (b) un checkpoint de la época 1 de DPO, (c) un promedio móvil exponencial (EMA) del modelo de política. Informa qué referencia produce la mayor precisión de preferencia y la curva de entrenamiento más estable.

Términos Clave

Término Lo que dice la gente Lo que realmente significa
DPO "RLHF sin RL" Direct Preference Optimization (Otimización de Preferencia Directa): un algoritmo de aprendizaje supervisado que optimiza el modelo de lenguaje directamente en pares de preferencias, omitiendo el modelo de recompensa y PPO
Recompensa implícita "La recompensa está en el modelo" La función de recompensa está determinada por la relación de probabilidad logarítmica entre la política y los modelos de referencia; no se necesita un modelo de recompensa separado
Beta (DPO) "La temperatura" Controla qué tan lejos puede desviarse la política del modelo de referencia; un beta pequeño permite grandes desviaciones, un beta grande mantiene el modelo cerca
Relación de probabilidad logarítmica "Qué tanto cambió el modelo" log pi(y|x) - log pi_ref(y|x) — un valor positivo significa que el modelo actual asigna una probabilidad más alta que la referencia
Modelo de referencia "El checkpoint congelado" Una copia del modelo SFT cuyos pesos nunca cambian; sirve como ancla para calcular las relaciones de probabilidad
KTO "DPO sin pares" Kahneman-Tversky Optimization (Optimización Kahneman-Tversky): funciona con etiquetas no pareadas de "bueno" o "malo" en lugar de requerir pares de preferencias
ORPO "Alineación en un solo paso" Odds Ratio Preference Optimization (Optimización de Preferencia por Relación de Posibilidades): combina SFT y alineación en un solo bucle de entrenamiento al agregar un término de preferencia a la pérdida de SFT
SimPO "No se necesita referencia" Simple Preference Optimization (Optimización de Preferencia Simple): elimina el modelo de referencia utilizando la probabilidad logarítmica promedio normalizada por la longitud como la recompensa implícita
Impuesto de alineación "El costo de hacer que los modelos sean seguros" El cómputo, datos y complejidad adicionales necesarios para pasar de un modelo base a un modelo alinos; DPO reduce esto significativamente

Lecturas Adicionales

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).