Phase 10 - Lesson 08

DPO: Direct Preference Optimization

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

O RLHF funciona. Ele também exige o treinamento de três modelos (SFT, modelo de recompensa, política), o gerenciamento da instabilidade do PPO e o ajuste de uma penalidade KL. O DPO pergunta: e se você pudesse pular tudo isso? O DPO otimiza diretamente o modelo de linguagem em pares de preferência. Sem modelo de recompensa. Sem PPO. Um loop de treinamento. Mesmos resultados.

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

Objetivos de Aprendizado

  • Implementar o treinamento DPO que otimiza diretamente um modelo de linguagem em pares de preferência sem um modelo de recompensa separado
  • Derivar a função de perda do DPO e explicar como ela representa implicitamente um modelo de recompensa por meio das probabilidades logarítmicas da política
  • Comparar DPO vs RLHF em termos de estabilidade de treinamento, custo computacional e número de modelos necessários
  • Ajustar o parâmetro beta para controlar o quanto a política treinada diverge do modelo de referência

O Problema

Você construiu um pipeline de RLHF na Lição 07. Três etapas. Três modelos. O modelo SFT, o modelo de recompensa e o modelo de política otimizado com PPO. O modelo de recompensa por si só exigiu milhares de pares de preferências humanas e um loop de treinamento separado. O PPO exigiu um ajuste cuidadoso do coeficiente KL, taxa de aprendizado, razão de corte (clip ratio) e número de épocas.

Na prática, o treinamento do PPO é notoriamente instável. Pequenas mudanças de hiperparâmetros fazem com que o treinamento divirja. O modelo de recompensa é um proxy imperfeito para as preferências humanas, e a política encontra maneiras de explorar suas fraquezas. A penalidade KL ajuda, mas exige seu próprio ajuste -- muito baixa e você obtém hacking de recompensa (reward hacking), muito alta e o modelo mal aprende.

Essa complexidade é o motivo pelo qual a maioria dos modelos de código aberto teve dificuldades com o RLHF por anos após a publicação do InstructGPT. O pipeline de três etapas é frágil. Cada etapa tem seus próprios modos de falha e os erros se acumulam.

Em maio de 2023, Rafael Rafailov, Archit Sharma e colegas de Stanford publicaram "Direct Preference Optimization: Your Language Model is Secretly a Reward Model". A principal percepção: você não precisa de um modelo de recompensa separado. A função de recompensa ideal é determinada matematicamente pelas próprias probabilidades de token do modelo de linguagem. Você pode pular totalmente o modelo de recompensa e otimizar o modelo de linguagem diretamente em pares de preferência.

O DPO reduz o RLHF a uma única etapa de aprendizado supervisionado. Um modelo. Uma função de perda. Um loop de treinamento. Sem aprendizado por reforço. O Zephyr-7B, um dos primeiros modelos a usar DPO em escala, igualou ou superou modelos treinados com RLHF completo em vários benchmarks. A Meta usou o DPO como parte do pipeline de alinhamento do Llama 3. A Anthropic citou métodos no estilo DPO em suas pesquisas de alinhamento.

O Conceito

A Percepção Principal

O RLHF otimiza este objetivo:

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

onde R é o modelo de recompensa, pi é a política, pi_ref é o modelo de referência e beta é o coeficiente KL.

O artigo do DPO mostrou que este objetivo tem uma solução ideal de forma fechada. Para qualquer função de recompensa R, the política ideal é:

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

onde Z(x) é uma constante de normalização. Reorganizando:

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

Este é o avanço. A recompensa é expressa inteiramente em termos das probabilidades do modelo de política e das probabilidades do modelo de referência. Você não precisa treinar um modelo de recompensa separado. A recompensa é implícita na razão de probabilidade.

Substituindo isso no modelo de preferência 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)))

Os termos Z(x) se cancelam porque ambas as respostas são condicionadas ao mesmo prompt x. O que resta é uma função que depende apenas das probabilidades logarítmicas do modelo de política e das probabilidades logarítmicas do modelo de referência para as respostas preferida e rejeitada.

A Perda 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))))

Vamos detalhar cada parte:

  • y_w = resposta preferida (vencedora)
  • y_l = resposta rejeitada (perdedora)
  • x = prompt
  • pi = modelo atual (sendo treinado)
  • pi_ref = modelo de referência (checkpoint SFT congelado)
  • beta = parâmetro de temperatura que controla o desvio em relação à referência (normalmente de 0,1 a 0,5)

A proporção log pi(y|x) / pi_ref(y|x) é a razão de probabilidade logarítmica. Quando essa proporção é positiva, o modelo atual atribui maior probabilidade à resposta y do que a referência. Quando negativa, o modelo atual atribui menor probabilidade.

A perda DPO força o modelo a aumentar a razão de probabilidade logarítmica para as respostas preferidas e a diminuí-la para as respostas rejeitadas. O parâmetro beta controla o quão agressivamente o modelo pode desviar da referência -- um beta pequeno significa que grandes desvios são permitidos, um beta grande mantém o modelo próximo à referência.

graph TD
    subgraph DPO["Treinamento DPO"]
        direction TB
        D["Dataset de Preferências\n(prompt, vencedor, perdedor)"] --> P1["Calcular log P(vencedor)\nsob o modelo atual"]
        D --> P2["Calcular log P(perdedor)\nsob o modelo atual"]
        D --> R1["Calcular log P(vencedor)\nsob o modelo de referência"]
        D --> R2["Calcular log P(perdedor)\nsob o modelo de referência"]

        P1 --> RATIO_W["Razão logarítmica (vencedor)\nlog pi/pi_ref"]
        R1 --> RATIO_W
        P2 --> RATIO_L["Razão 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["Atualização de gradiente\nno modelo atual"]
    end

    subgraph Models["Modelos"]
        PI["Modelo Atual (pi)\natualizado a cada etapa"]
        REF["Modelo de Referência (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 que o DPO é mais simples

Aspecto RLHF (PPO) DPO
Modelos para treinar 3 (SFT + recompensa + política) 1 (apenas política)
Loops de treinamento 3 (SFT, treinamento do RM, PPO) 2 (SFT, DPO)
Hiperparâmetros lr, coef KL, taxa de corte, lr do RM, épocas x3 lr, beta, épocas
Modelo de recompensa Necessário (treinamento separado) Implícito nas probabilidades do modelo
Algoritmo de RL PPO (complexo, instável) Aprendizado supervisionado (estável)
Memória de GPU 3-4 modelos na memória durante o PPO 2 modelos (atual + referência)
Estabilidade de treinamento Sensível a hiperparâmetros Robusto, semelhante ao SFT

O DPO precisa de dois modelos na memória durante o treinamento -- o modelo atual e a referência congelada. O RLHF precisa de três ou quatro: a política, a referência, o modelo de recompensa e, opcionalmente, uma linha de base da função de valor (value function baseline). Para um modelo 70B, cada cópia consome 140 GB em FP16. A economia de memória ao eliminar o modelo de recompensa é substancial.

Quando o DPO Supera o RLHF

Conjuntos de dados pequenos. Com 5.000 a 20.000 pares de preferências, o DPO muitas vezes se equipara ou supera o RLHF. O modelo de recompensa no RLHF precisa de dados suficientes para generalizar -- com dados limitados, ele sofre overfitting e produz sinais de recompensa não confiáveis. O DPO contorna esse problema ao não necessitar de nenhum modelo de recompensa.

Computação limitada. O DPO requer cerca de um terço da computação do RLHF completo (um loop de treinamento em vez de três). Para equipes sem grandes clusters de GPU, essa é a escolha prática.

Iteração rápida. Quer testar 10 conjuntos de dados de preferências diferentes para ver qual produz o melhor modelo? O DPO permite que você execute cada experimento em poucas horas. O RLHF exige treinar novamente o modelo de recompensa para cada conjunto de dados.

Quando o RLHF Supera o DPO

Treinamento em grande escala. Na escala do GPT-4 ou Claude, o modelo de recompensa separado do RLHF pode capturar sinais de preferência mais sutis. O modelo de recompensa atua como uma função de perda aprendida que se adapta a critérios de qualidade complexos.

Sinais de recompensa complexos. Quando "melhor" envolve múltiplas dimensões (utilidade, inofensividade, honestidade), um modelo de recompensa pode aprender esse equilíbrio de múltiplos objetivos. O DPO trata cada par de preferência como um sinal binário -- uma é melhor, outra é pior -- sem modelar o porquê.

Alinhamento iterativo. Os pipelines de RLHF podem gerar novas respostas com a política atual, fazer com que humanos as avaliem e treinar novamente o modelo de recompensa em um loop online. O DPO trabalha em um conjunto de dados fixo de pares de preferências. O Constitutional AI (abordagem da Anthropic) utiliza essa propriedade iterativa do RLHF extensivamente.

Além do DPO: KTO, ORPO, SimPO

O DPO inspirou uma família de métodos de alinhamento simplificados.

KTO (Kahneman-Tversky Optimization, 2024): Você nem precisa de pares. O KTO funciona com feedback não pareado -- basta rotular cada resposta como "boa" ou "ruim" sem compará-la a uma alternativa. Isso simplifica drasticamente a coleta de dados. Em vez de mostrar aos anotadores duas respostas e perguntar "qual é a melhor?", você mostra uma resposta e pergunta "esta resposta é boa?". A função de perda aplica a aversão à perda da teoria prospectiva: as respostas ruins são mais penalizadas do que as respostas boas são recompensadas.

ORPO (Odds Ratio Preference Optimization, 2024): Combina SFT e alinhamento em uma única etapa de treinamento. Em vez de fazer primeiro o SFT e depois o DPO, o ORPO modifica a perda do SFT para incluir um sinal de preferência. A perda possui dois termos: uma perda padrão de predição do próximo token nas respostas preferidas, mais um termo de razão de chances (odds ratio) que aumenta a lacuna entre as probabilidades das respostas preferidas e rejeitadas. Um loop de treinamento em vez de dois.

SimPO (Simple Preference Optimization, 2024): Elimina completamente o modelo de referência. Em vez de calcular razões de probabilidade logarítmica em relação a uma referência congelada, o SimPO usa a probabilidade logarítmica média da resposta (normalizada pelo comprimento) como a recompensa implícita. Isso economiza memória (nenhum modelo de referência é necessário) e simplifica o treinamento. A normalização de comprimento evita que o modelo favoreça respostas mais curtas.

Método Ano Modelos na memória Precisa de pares? Precisa de referência? Loops de treinamento
RLHF 2022 3-4 Sim (para RM) Sim 3
DPO 2023 2 Sim Sim 2
KTO 2024 2 Não (não pareado) Sim 2
ORPO 2024 1 Sim Não 1
SimPO 2024 1 Sim Não 1

A tendência é clara: cada método elimina mais uma peça de complexidade. O RLHF precisava de um modelo de recompensa e do PPO. O DPO eliminou ambos. O KTO eliminou os dados pareados. O ORPO eliminou a etapa de SFT separada. O SimPO eliminou o modelo de referência. A "taxa de alinhamento" (alignment tax) -- o custo computacional e de complexidade para passar de um modelo base para um modelo alinhado -- continua caindo.

Implementações Reais do DPO

Zephyr-7B (HuggingFace, Outubro de 2023): Base Mistral 7B, SFT no UltraChat (200 mil exemplos) e depois DPO no UltraFeedback (60 mil pares de preferências). Obteve nota 6,47 no MT-Bench -- o modelo 7B com maior pontuação na época. Para comparação, o Llama 2 Chat 70B obteve 6,86, significando que o Zephyr ficou a 6% de distância de um modelo 10 vezes maior usando apenas o alinhamento DPO.

Llama 3 (Meta, Abril de 2024): Usou DPO após as etapas iniciais de RLHF. A combinação sugere que o DPO e o RLHF podem ser complementares -- RLHF para um alinhamento amplo e DPO para refinamento direcionado.

Neural Magic / nm-chat (2024): Aplicou DPO a múltiplos modelos de código aberto, mostrando de forma consistente uma melhoria de 5% a 15% em benchmarks de alinhamento em comparação com baselines apenas com SFT.

Construa

Passo 1: Dataset de Preferências

Mesmo formato que o RLHF -- triplas (prompt, preferred, rejected). O DPO consome esses dados diretamente sem um modelo de recompensa intermediário.

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.",
    },
]

Passo 2: Probabilidade Logarítmica da Sequência

A perda DPO requer o cálculo da probabilidade logarítmica total de uma resposta dado um prompt. Isso significa executar o modelo em toda a sequência (prompt + resposta) e somar as probabilidades logarítmicas de cada token de resposta.

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 função é a base de execução do DPO. Para cada par de preferência, ela é executada quatro vezes: o modelo na resposta preferida, o modelo na resposta rejeitada, a referência na resposta preferida e a referência na resposta rejeitada. Isso representa 4 passagens diretas (forward passes) por exemplo de treinamento, em contraste com a geração + pontuação de recompensa + estimativa de valor + atualização do PPO no RLHF. Mais simples, mais rápido e mais estável.

Passo 3: A Perda DPO

O núcleo do artigo em código. Uma função. Uma perda. Sem 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),
    }

Os valores de preferred_ratio e rejected_ratio são as razões de probabilidade logarítmica da derivação do DPO. Quando o modelo atual atribui maior probabilidade à resposta preferida (em relação à referência) e menor probabilidade à resposta rejeitada, o logit é positivo e a perda é baixa. O sinal de treinamento direciona o modelo exatamente nesse sentido.

O implicit_preferred_reward e o implicit_rejected_reward são as recompensas que a perda DPO atribui implicitamente. Você pode extraí-los para verificar se o treinamento está funcionando -- a margem entre as recompensas preferidas e rejeitadas deve aumentar ao longo do treinamento.

Passo 4: Loop de Treinamento DPO

Um loop de treinamento supervisionado padrão. Sem PPO. Sem modelo de recompensa. Apenas passagens diretas (forward passes) e atualizações 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

O loop de treinamento é incrivelmente simples se comparado ao RLHF. Para cada par de preferência: calcula-se quatro probabilidades logarítmicas (dois modelos, duas respostas), insere-se as mesmas na perda DPO, calcula-se o gradiente e atualiza-se a política. Nenhuma etapa de geração. Nenhuma inferência do modelo de recompensa. Nenhuma estimativa de vantagem. Nenhum corte (clipping).

Passo 5: Comparar DPO vs RLHF

Meça as margens de recompensa implícitas e os desvios de probabilidade logarítmica para comparar o DPO com o modelo RLHF da Lição 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()

Passo 6: Análise de Sensibilidade do Beta

O parâmetro beta é o equivalente no DPO ao coeficiente KL no RLHF. Ele controla o quanto o modelo pode desviar da referência. Este experimento demonstra o seu efeito.

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

Um beta pequeno (0,01) permite que o modelo desvie livremente da referência -- aprendizado rápido, mas com risco de soluções degeneradas. Um beta grande (1,0) mantém o modelo próximo à referência -- aprendizado estável, mas lento. O ponto ideal para a maioria das aplicações fica entre 0,1 e 0,3.

Use

Demonstração do 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.")

Entregue

Esta lição produz outputs/prompt-alignment-method-selector.md -- um prompt que ajuda você a escolher o método de alinhamento correto (SFT, RLHF, DPO, KTO, ORPO, SimPO) para o seu caso de uso. Com base na sua disponibilidade de dados, orçamento computacional e objetivos de alinhamento, ele recomenda um método e um plano de treinamento.

Exercícios

  1. Implementar a KTO (Kahneman-Tversky Optimization). A KTO não precisa de pares -- basta rotular cada resposta como "boa" ou "ruim". A perda para uma resposta boa é -log(sigmoid(beta * log_ratio)) e para uma resposta ruim é -log(1 - sigmoid(beta * log_ratio)) com um multiplicador de aversão à perda (normalmente de 1,5x) sobre a perda da resposta ruim. Treine com os mesmos dados (trate as preferidas como "boas" e as rejeitadas como "ruins" de forma independente) e compare a acurácia com o DPO.

  2. Implementar o DPO normalizado pelo comprimento. Em vez de probabilidades logarítmicas brutas, divida pelo número de tokens de resposta: normalized_logprob = total_logprob / num_tokens. Isso evita que o modelo favoreça respostas mais curtas (que têm probabilidade logarítmica total maior). Compare as margens de recompensa implícitas com e sem normalização.

  3. Construir uma perda combinada no estilo ORPO. Adicione uma perda de predição do próximo token padrão na resposta preferida à perda do DPO: L = L_sft(preferred) + alpha * L_dpo. Experimente valores de alpha de 0,1, 0,5 e 1,0. A perda combinada deve produzir um modelo que siga instruções (do termo SFT) e prefira respostas melhores (do termo DPO), eliminando a necessidade de uma etapa de SFT separada.

  4. Implementar DPO iterativo. Execute o DPO por 3 épocas, depois gere novas respostas a partir do modelo treinado, emparelhe-as com as respostas preferidas originais como novos pares de preferências e execute o DPO novamente. Realize duas rodadas desse processo de "auto-jogo" (self-play). Compare a acurácia de preferência após a rodada 1 e a rodada 2 para ver se o refinamento iterativo ajuda.

  5. Comparar DPO com diferentes modelos de referência. Em vez de usar o checkpoint SFT como referência, tente: (a) o modelo base (pré-SFT), (b) um checkpoint da época 1 do DPO, (c) uma média móvel exponencial (EMA) do modelo de política. Relate qual referência produz a maior acurácia de preferência e a curva de treinamento mais estável.

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
DPO "RLHF sem RL" Otimização Direta de Preferência: um algoritmo de aprendizado supervisionado que otimiza o modelo de linguagem diretamente em pares de preferências, ignorando o modelo de recompensa e o PPO
Recompensa implícita "A recompensa está no modelo" A função de recompensa é determinada pela razão de probabilidade logarítmica entre os modelos de política e de referência -- nenhum modelo de recompensa separado é necessário
Beta (DPO) "A temperatura" Controla o quão longe a política pode desviar do modelo de referência -- um beta pequeno permite grandes desvios, um beta grande mantém o modelo próximo
Razão de probabilidade logarítmica "O quanto o modelo mudou" log pi(y|x) - log pi_ref(y|x) -- positivo significa que o modelo atual atribui maior probabilidade do que a referência
Modelo de referência "O checkpoint congelado" Uma cópia do modelo SFT cujos pesos nunca mudam -- serve como âncora para calcular as razões de probabilidade
KTO "DPO sem pares" Otimização Kahneman-Tversky: funciona com rótulos de feedback não pareados ("bom" ou "ruim") em vez de exigir pares de preferências
ORPO "Alinhamento em uma etapa" Otimização de Preferência por Razão de Chances: combina SFT e alinhamento em um único loop de treinamento, adicionando um termo de preferência à perda de SFT
SimPO "Nenhuma referência necessária" Otimização de Preferência Simples: elimina o modelo de referência usando a probabilidade logarítmica média normalizada pelo comprimento como a recompensa implícita
Taxa de alinhamento "O custo de tornar os modelos seguros" O custo computacional, de dados e complexidade adicional necessário para ir de um modelo base para um modelo alinhado -- o DPO reduz isso significativamente

Leituras Adicionais

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