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
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.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.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.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.
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
- Rafailov et al., 2023 -- "Direct Preference Optimization: Your Language Model is Secretly a Reward Model" -- o artigo do DPO que simplificou o alinhamento de RLHF para aprendizado supervisionado
- Tunstall et al., 2023 -- "Zephyr: Direct Distillation of LM Alignment" -- Zephyr-7B, demonstrando que o DPO no UltraFeedback se equipara ao RLHF em benchmarks
- Ethayarajh et al., 2024 -- "KTO: Model Alignment as Prospect Theoretic Optimization" -- eliminando a necessidade de preferências pareadas
- Hong et al., 2024 -- "ORPO: Monolithic Preference Optimization without Reference Model" -- combinando SFT e alinhamento em uma única etapa
- Meng et al., 2024 -- "SimPO: Simple Preference Optimization with a Reference-Free Reward" -- eliminando completamente o modelo de referência
- Llama 3 Technical Report -- o pipeline de alinhamento da Meta que combina RLHF e DPO