Phase 10 - Lesson 07
RLHF: Modelo de Recompensa + PPO
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
O SFT ensina o modelo a seguir instruções. Mas não ensina qual resposta é MELHOR. Duas respostas gramaticalmente corretas e factualmente precisas podem diferir enormemente em utilidade. O RLHF é como você codifica o julgamento humano no comportamento do modelo. É o que torna o Claude útil e o GPT educado.
Tipo: Construção Linguagens: Python (com numpy) Pré-requisitos: Fase 10, Lição 06 (Ajuste de Instrução / SFT) Tempo: ~90 minutos
Objetivos de Aprendizado
- Construir um modelo de recompensa que avalia a qualidade da resposta a partir de pares de preferências humanas (escolhido vs rejeitado)
- Implementar o loop de treinamento PPO que otimiza uma política de modelo de linguagem em relação ao modelo de recompensa com uma penalidade KL
- Explicar por que o RLHF requer três modelos (SFT, recompensa, política) e como a restrição KL previne o "reward hacking" (fraude de recompensa)
- Avaliar o efeito do RLHF comparando a qualidade da resposta antes e depois da otimização de preferências
O Problema
Pergunte a um modelo "Explique a computação quântica" e ele pode produzir:
Resposta A: "A computação quântica usa qubits que podem existir em superposição, o que significa que podem ser 0, 1 ou ambos simultaneamente. Isso permite que os computadores quânticos processem certos cálculos exponencialmente mais rápido do que os computadores clássicos. Os principais algoritmos incluem o algoritmo de Shor para fatorar números grandes e o algoritmo de Grover para buscar em bancos de dados não ordenados."
Resposta B: "A computação quântica é um tipo de computação que usa fenômenos da mecânica quântica. Foi proposta pela primeira vez na década de 1980. Richard Feynman sugeriu que sistemas quânticos poderiam ser simulados por computadores quânticos. O campo cresceu significativamente desde então. Muitas empresas estão agora trabalhando em computadores quânticos. IBM, Google e outros fizeram progressos. A supremacia quântica foi reivindicada pelo Google em 2019."
Ambas as respostas são factualmente corretas. Ambas são gramaticalmente corretas. Ambas seguem a instrução. Mas a Resposta A é claramente melhor. É mais concisa, mais informativa e melhor estruturada. Um ser humano escolheria a A todas as vezes.
O SFT não consegue capturar essa distinção. Ele treina o modelo em respostas "corretas", mas não tem nenhum mecanismo para dizer "esta resposta é melhor do que aquela". Ele trata cada exemplo de treinamento como igualmente bom. Se tanto A quanto B aparecessem no dataset de SFT, o modelo aprenderia com ambos igualmente.
O RLHF resolve isso. Ele treina um modelo de recompensa para prever qual resposta um ser humano preferiria, e então usa esse sinal de recompensa para empurrar o modelo de linguagem em direção a saídas de maior qualidade. O InstructGPT (o precursor do ChatGPT) usou RLHF para melhorar drasticamente a utilidade, veracidade e inofensividade do GPT-3. Os avaliadores internos da OpenAI preferiram as saídas do InstructGPT em relação às saídas do GPT-3 em 85% das vezes, apesar de o InstructGPT ser 135 vezes menor (1,3B vs 175B parâmetros).
O Conceito
Os Três Estágios
O RLHF não é uma única execução de treinamento. É um pipeline de três estágios sequenciais, cada um construído sobre o anterior.
Estágio 1: SFT. Treina um modelo base em pares de instrução-resposta (Lição 06). Isso fornece um modelo que pode seguir instruções, mas não sabe quais respostas são melhores do que outras.
Estágio 2: Modelo de Recompensa. Coleta dados de preferência humana: mostra aos anotadores duas respostas para o mesmo prompt e pergunta "qual é melhor?". Treina um modelo para prever essas preferências. O modelo de recompensa recebe (prompt, resposta) como entrada e fornece um score escalar como saída.
Estágio 3: PPO. Usa o modelo de recompensa para gerar um sinal de treinamento para o modelo de linguagem. O modelo de linguagem gera respostas, o modelo de recompensa as avalia, e o PPO atualiza o modelo de linguagem para produzir respostas com pontuações mais altas. Uma penalidade de divergência KL impede que o modelo de linguagem se afaste muito do checkpoint de SFT.
graph TD
subgraph Stage1["Estágio 1: SFT"]
B["Modelo Base"] --> S["Modelo SFT"]
D["Dados de Instrução\n(27 mil exemplos)"] --> S
end
subgraph Stage2["Estágio 2: Modelo de Recompensa"]
S --> |"Gerar respostas"| P["Pares de Preferência\n(prompt, vencedor, perdedor)"]
H["Anotadores Humanos"] --> P
P --> R["Modelo de Recompensa\nR(prompt, resposta) → score"]
end
subgraph Stage3["Estágio 3: PPO"]
S --> |"Inicializar política"| PI["Modelo de Política\n(sendo otimizado)"]
S --> |"Congelar como referência"| REF["Modelo de Referência\n(SFT congelado)"]
PI --> |"Gerar"| RESP["Resposta"]
RESP --> R
R --> |"Sinal de recompensa"| PPO["Atualização PPO"]
REF --> |"Penalidade KL"| PPO
PPO --> |"Atualizar"| PI
end
style S fill:#1a1a2e,stroke:#51cf66,color:#fff
style R fill:#1a1a2e,stroke:#e94560,color:#fff
style PI fill:#1a1a2e,stroke:#0f3460,color:#fff
style REF fill:#1a1a2e,stroke:#0f3460,color:#fff
style PPO fill:#1a1a2e,stroke:#e94560,color:#fff
O Modelo de Recompensa
O modelo de recompensa é um modelo de linguagem adaptado como um avaliador. Pegue o modelo SFT, substitua a cabeça de modelagem de linguagem (que gera uma distribuição sobre o vocabulário) por uma cabeça escalar (que gera um único número). A arquitetura é idêntica até a camada final.
Entrada: um prompt concatenado com uma resposta. Saída: um único score escalar de recompensa.
Os dados de treinamento são pares de preferências humanas. Para cada prompt, os anotadores veem duas respostas e escolhem a melhor. Isso cria trios de treinamento: (prompt, resposta_preferida, resposta_rejeitada).
A função de perda usa o modelo Bradley-Terry de preferências pareadas:
loss = -log(sigmoid(reward(preferred) - reward(rejected)))
Esta é a equação fundamental. sigmoid(reward(A) - reward(B)) fornece a probabilidade de a resposta A ser preferida em relação à resposta B. A perda empurra o modelo de recompensa para atribuir um score mais alto à resposta preferida.
Por que comparações pareadas em vez de pontuações absolutas? Porque os humanos são péssimos em atribuir pontuações de qualidade absoluta ("Esta resposta é um 7,3 ou um 7,5 de 10?"), mas muito bons em comparações relativas ("A é melhor que B?"). O modelo Bradley-Terry converte comparações relativas em um sistema de pontuação absoluta consistente.
Números do InstructGPT: A OpenAI coletou 33.000 pares de comparação de 40 contratados. Cada comparação levou cerca de 5 minutos. Isso representa 2.750 horas de trabalho humano para os dados de treinamento do modelo de recompensa.
PPO: Otimização de Política Próxima
O PPO é um algoritmo de aprendizado por reforço. No RLHF, o "ambiente" é o modelo de recompensa, o "agente" é o modelo de linguagem e a "ação" é a geração de um token.
O objetivo:
maximize: E[R(prompt, response)] - beta * KL(policy || reference)
O primeiro termo empurra o modelo a gerar respostas de alta recompensa. O segundo termo (penalidade de divergência KL) impede que o modelo se desvie muito do checkpoint de SFT.
Por que a penalidade KL? Sem ela, o modelo encontra soluções degeneradas. O modelo de recompensa é treinado em um dataset finito de preferências humanas. Ele tem pontos cegos. O modelo de linguagem irá explorar esses pontos cegos — encontrando saídas que pontuam alto no modelo de recompensa, mas na verdade são absurdas. Exemplos clássicos:
- Repetir "Sou tão prestativo e inofensivo!" pontua alto em modelos de recompensa de utilidade/inofensividade
- Produzir respostas prolixas, de tom formal, mas vazias, que correspondem ao padrão de "alta qualidade"
- Explorar frases específicas que por acaso correlacionaram com alta recompensa nos dados de treinamento
A penalidade KL diz: você pode melhorar, mas não pode se tornar um modelo completamente diferente. Permaneça próximo da versão SFT, que já era razoável. Afaste-se demais e o custo de KL dominará a recompensa.
Números do InstructGPT: O treinamento PPO usou lr=1.5e-5, coeficiente KL beta=0.02, 256 mil episódios (pares de prompt-resposta) e 4 épocas PPO por lote. Todo o pipeline de RLHF levou vários dias em um clúster de GPUs.
graph LR
subgraph PPO["Loop de Treinamento PPO"]
direction TB
PROMPT["Amostra de prompt\ndo dataset"] --> GEN["Política gera\nresposta"]
GEN --> SCORE["Modelo de recompensa\navalia resposta"]
GEN --> KL["Calcular divergência KL\nvs modelo de referência"]
SCORE --> OBJ["Objetivo:\nrecompensa - beta * KL"]
KL --> OBJ
OBJ --> UPDATE["Atualização de gradiente PPO\n(função de perda substituta limitada)"]
UPDATE --> |"repetir"| PROMPT
end
style PROMPT fill:#1a1a2e,stroke:#0f3460,color:#fff
style SCORE fill:#1a1a2e,stroke:#51cf66,color:#fff
style KL fill:#1a1a2e,stroke:#e94560,color:#fff
style OBJ fill:#1a1a2e,stroke:#e94560,color:#fff
O Objetivo do PPO em Detalhes
O PPO usa um "objetivo substituto limitado" (clipped surrogate objective) para evitar atualizações excessivamente grandes. A razão entre as probabilidades da nova política e da antiga política é limitada ao intervalo [1 - epsilon, 1 + epsilon], onde epsilon é tipicamente 0.2.
ratio = pi_new(action | state) / pi_old(action | state)
clipped_ratio = clip(ratio, 1 - epsilon, 1 + epsilon)
loss = -min(ratio * advantage, clipped_ratio * advantage)
A função de vantagem (advantage function) estima o quanto a resposta atual é melhor em comparação com a qualidade esperada. No RLHF:
advantage = reward(prompt, response) - baseline
O baseline é frequentemente a média de recompensa sobre as respostas recentes. Uma vantagem positiva significa que a resposta foi melhor do que a média; uma vantagem negativa significa que foi pior. O PPO aumenta a probabilidade de respostas acima da média e diminui a probabilidade de respostas abaixo da média.
A limitação (clipping) evita atualizações catastróficas. Se uma única resposta receber uma recompensa excepcionalmente alta, a razão não limitada pode ser muito grande, fazendo com que o modelo mude drasticamente em direção a essa resposta. O clipping limita a atualização, mantendo a estabilidade do treinamento.
Reward Hacking
O lado sombrio do RLHF. O modelo de linguagem está otimizando em relação ao modelo de recompensa, que é uma aproximação imperfeita das preferências humanas. Conforme o modelo de linguagem melhora em maximizar a recompensa, ele começa a explorar as fraquezas do modelo de recompensa.
Modos de falha comuns:
| Falha | O que acontece | Por quê |
|---|---|---|
| Prolixidade (Verbosity) | O modelo produz respostas cada vez mais longas | Os anotadores humanos frequentemente preferiam respostas mais longas e detalhadas, então o modelo de recompensa atribui scores mais altos ao comprimento |
| Sicofancia (Sycophancy) | O modelo concorda com tudo o que o usuário diz | Os anotadores preferiram respostas que concordassem com a premissa da pergunta |
| Esquiva (Hedging) | O modelo se recusa a se comprometer com uma resposta | Respostas esquivas ("Este é um tópico complexo com muitas perspectivas...") raramente são marcadas como erradas |
| Formatação excessiva (Format gaming) | O modelo usa marcadores e cabeçalhos excessivamente | Respostas formatadas pareciam mais "polidas" para os anotadores |
Estratégias de mitigação: penalidade KL mais forte (impede que o modelo se afaste o suficiente para explorar fraquezas), treinamento do modelo de recompensa em exemplos adversários (corrige modos de falha conhecidos) e uso de múltiplos modelos de recompensa com arquiteturas diferentes (mais difícil de burlar todos simultaneamente).
Pipelines Reais de RLHF
| Modelo | Pares de Comparação | Anotadores | Tamanho do RM | Passos PPO | Coef. KL |
|---|---|---|---|---|---|
| InstructGPT | 33K | 40 | 6B | 256K | 0.02 |
| Llama 2 Chat | ~1M | não divulgado | 70B | não divulgado | 0.01 |
| Claude | não divulgado | não divulgado | não divulgado | não divulgado | não divulgado |
| Artigo RLHF da Anthropic | 22K | 20 | 52B | 50K | 0.001 |
O artigo de 2022 da Anthropic treinou um modelo de recompensa de 52B em 22.000 comparações. Modelos de recompensa maiores produzem sinais mais confiáveis, o que torna o treinamento PPO mais estável. Usar um modelo de recompensa pequeno para treinar um modelo de linguagem grande é arriscado — o modelo de recompensa não tem capacidade suficiente para capturar as nuances de respostas boas vs ruins.
Construa Você Mesmo
Passo 1: Dados Sintéticos de Preferência
Na produção, anotadores humanos criam dados de preferência. Nós criaremos pares sintéticos onde a resposta "preferida" é objetivamente melhor (mais concisa, mais precisa, mais útil).
import numpy as np
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.",
},
]
As respostas preferidas são concisas e diretas. As respostas rejeitadas exibem modos de falha comuns: preenchimento desnecessário, hesitação/esquiva, explicações redundantes e imprecisão. Este é exatamente o tipo de distinção que o SFT não consegue capturar, mas o RLHF consegue.
Passo 2: Arquitetura do Modelo de Recompensa
O modelo de recompensa reutiliza a arquitetura do transformer do mini GPT, mas substitui a cabeça de saída do tamanho do vocabulário por uma única projeção escalar.
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
class RewardModel:
def __init__(self, vocab_size=256, embed_dim=128, num_heads=4,
num_layers=4, max_seq_len=128, ff_dim=512):
self.embedding = Embedding(vocab_size, embed_dim, max_seq_len)
self.blocks = [
TransformerBlock(embed_dim, num_heads, ff_dim)
for _ in range(num_layers)
]
self.ln_f = LayerNorm(embed_dim)
self.reward_head = np.random.randn(embed_dim) * 0.02
def forward(self, token_ids):
seq_len = token_ids.shape[-1]
mask = np.triu(np.full((seq_len, seq_len), -1e9), k=1)
x = self.embedding.forward(token_ids)
for block in self.blocks:
x = block.forward(x, mask)
x = self.ln_f.forward(x)
last_hidden = x[:, -1, :]
reward = last_hidden @ self.reward_head
return reward
O modelo de recompensa pega o estado oculto na última posição do token e o projeta em um escalar. Por que o último token? Porque a máscara de atenção causal significa que a última posição prestou atenção a cada token anterior. Ela possui a representação mais completa de toda a sequência (prompt, resposta).
Passo 3: Perda Bradley-Terry
Treine o modelo de recompensa em pares de preferência usando a perda pareada Bradley-Terry.
def tokenize_for_reward(prompt, response, vocab_size=256):
prompt_tokens = [min(t, vocab_size - 1) for t in list(prompt.encode("utf-8"))]
response_tokens = [min(t, vocab_size - 1) for t in list(response.encode("utf-8"))]
return prompt_tokens + [0] + response_tokens
def sigmoid(x):
return np.where(
x >= 0,
1.0 / (1.0 + np.exp(-x)),
np.exp(x) / (1.0 + np.exp(x))
)
def bradley_terry_loss(reward_preferred, reward_rejected):
diff = reward_preferred - reward_rejected
loss = -np.log(sigmoid(diff) + 1e-8)
return loss
def train_reward_model(rm, preference_data, num_epochs=10, lr=1e-4, max_seq_len=128):
print(f"Training Reward Model: {len(preference_data)} preference pairs, {num_epochs} epochs")
print()
losses = []
accuracies = []
for epoch in range(num_epochs):
epoch_loss = 0.0
epoch_correct = 0
num_pairs = 0
indices = np.random.permutation(len(preference_data))
for idx in indices:
pair = preference_data[idx]
preferred_tokens = tokenize_for_reward(pair["prompt"], pair["preferred"])
rejected_tokens = tokenize_for_reward(pair["prompt"], pair["rejected"])
preferred_tokens = preferred_tokens[:max_seq_len]
rejected_tokens = rejected_tokens[:max_seq_len]
preferred_ids = np.array(preferred_tokens).reshape(1, -1)
rejected_ids = np.array(rejected_tokens).reshape(1, -1)
r_preferred = rm.forward(preferred_ids)[0]
r_rejected = rm.forward(rejected_ids)[0]
loss = bradley_terry_loss(r_preferred, r_rejected)
if r_preferred > r_rejected:
epoch_correct += 1
diff = r_preferred - r_rejected
grad = sigmoid(diff) - 1.0
rm.reward_head -= lr * grad * rm.ln_f.forward(
rm.embedding.forward(preferred_ids)
)[:, -1, :].flatten()
epoch_loss += loss
num_pairs += 1
avg_loss = epoch_loss / max(num_pairs, 1)
accuracy = epoch_correct / max(num_pairs, 1)
losses.append(avg_loss)
accuracies.append(accuracy)
if epoch % 2 == 0:
print(f" Epoch {epoch + 1:3d} | Loss: {avg_loss:.4f} | Accuracy: {accuracy:.1%}")
return rm, losses, accuracies
A métrica de acurácia é direta: qual fração de pares de preferência o modelo de recompensa classifica corretamente? Um modelo aleatório pontua 50%. Um modelo de recompensa bem treinado em dados limpos deve exceder 70%. O modelo de recompensa do InstructGPT alcançou cerca de 72% de acurácia em comparações separadas para teste, o que parece baixo, mas na verdade é bom — muitos pares de preferência são ambíguos até mesmo para humanos (o acordo entre anotadores foi de cerca de 73%).
Passo 4: Loop PPO Simplificado
O PPO completo é complexo. Esta implementação captura o mecanismo principal: gerar respostas, avaliá-las, computar a vantagem e atualizar a política com uma penalidade KL.
def compute_kl_divergence(policy_logits, reference_logits):
policy_probs = np.exp(policy_logits - policy_logits.max(axis=-1, keepdims=True))
policy_probs = policy_probs / policy_probs.sum(axis=-1, keepdims=True)
policy_probs = np.clip(policy_probs, 1e-10, 1.0)
ref_probs = np.exp(reference_logits - reference_logits.max(axis=-1, keepdims=True))
ref_probs = ref_probs / ref_probs.sum(axis=-1, keepdims=True)
ref_probs = np.clip(ref_probs, 1e-10, 1.0)
kl = np.sum(policy_probs * np.log(policy_probs / ref_probs), axis=-1)
return kl.mean()
def generate_response(model, prompt_tokens, max_new_tokens=30, temperature=0.8, max_seq_len=128):
tokens = list(prompt_tokens)
for _ in range(max_new_tokens):
context = np.array(tokens[-max_seq_len:]).reshape(1, -1)
logits = model.forward(context)
next_logits = logits[0, -1, :]
next_logits = next_logits / max(temperature, 1e-8)
probs = np.exp(next_logits - next_logits.max())
probs = probs / probs.sum()
probs = np.clip(probs, 1e-10, 1.0)
probs = probs / probs.sum()
next_token = np.random.choice(len(probs), p=probs)
tokens.append(int(next_token))
return tokens
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 ppo_training(policy_model, reference_model, reward_model, prompts,
num_episodes=20, lr=1.5e-5, kl_coeff=0.02, max_seq_len=128):
print(f"PPO Training: {num_episodes} episodes, lr={lr}, KL coeff={kl_coeff}")
print()
rewards_history = []
kl_history = []
for episode in range(num_episodes):
prompt_text = prompts[episode % len(prompts)]
prompt_tokens = [min(t, 252) for t in list(prompt_text.encode("utf-8"))]
response_tokens = generate_response(
policy_model, prompt_tokens,
max_new_tokens=20, temperature=0.8, max_seq_len=max_seq_len
)
response_ids = np.array(response_tokens[:max_seq_len]).reshape(1, -1)
reward = reward_model.forward(response_ids)[0]
policy_logits = policy_model.forward(response_ids)
ref_logits = reference_model.forward(response_ids)
kl = compute_kl_divergence(policy_logits, ref_logits)
total_reward = reward - kl_coeff * kl
rewards_history.append(float(reward))
kl_history.append(float(kl))
for block in policy_model.blocks:
update_scale = lr * total_reward
block.ffn.W1 += update_scale * np.random.randn(*block.ffn.W1.shape) * 0.01
block.ffn.W2 += update_scale * np.random.randn(*block.ffn.W2.shape) * 0.01
if episode % 5 == 0:
avg_reward = np.mean(rewards_history[-5:]) if rewards_history else 0
avg_kl = np.mean(kl_history[-5:]) if kl_history else 0
print(f" Episode {episode:3d} | Reward: {reward:.4f} | KL: {kl:.4f} | "
f"Avg Reward: {avg_reward:.4f}")
return policy_model, rewards_history, kl_history
O loop principal: (1) amostrar um prompt, (2) gerar uma resposta, (3) avaliá-la com o modelo de recompensa, (4) computar a divergência KL em relação à referência congelada, (5) computar a recompensa ajustada (recompensa menos a penalidade KL), (6) atualizar a política. A penalidade KL aumenta à medida que a política diverge da referência, prevenindo automaticamente o reward hacking.
Passo 5: Comparação de Pontuação de Recompensa
Após o RLHF, as respostas do modelo de política devem pontuar mais alto no modelo de recompensa do que as respostas do modelo SFT original.
def compare_models(sft_model, rlhf_model, reward_model, prompts, max_seq_len=128):
print("Model Comparison (reward scores)")
print("-" * 60)
print(f" {'Prompt':<35} {'SFT':>10} {'RLHF':>10}")
print(" " + "-" * 55)
sft_total = 0.0
rlhf_total = 0.0
for prompt in prompts:
prompt_tokens = [min(t, 252) for t in list(prompt.encode("utf-8"))]
sft_response = generate_response(
sft_model, prompt_tokens,
max_new_tokens=20, temperature=0.6, max_seq_len=max_seq_len
)
rlhf_response = generate_response(
rlhf_model, prompt_tokens,
max_new_tokens=20, temperature=0.6, max_seq_len=max_seq_len
)
sft_ids = np.array(sft_response[:max_seq_len]).reshape(1, -1)
rlhf_ids = np.array(rlhf_response[:max_seq_len]).reshape(1, -1)
sft_reward = reward_model.forward(sft_ids)[0]
rlhf_reward = reward_model.forward(rlhf_ids)[0]
sft_total += sft_reward
rlhf_total += rlhf_reward
truncated_prompt = prompt[:33] + ".." if len(prompt) > 35 else prompt
print(f" {truncated_prompt:<35} {sft_reward:>10.4f} {rlhf_reward:>10.4f}")
n = len(prompts)
print(" " + "-" * 55)
print(f" {'Average':<35} {sft_total/n:>10.4f} {rlhf_total/n:>10.4f}")
return sft_total / n, rlhf_total / n
Use-o
Demonstração do Pipeline Completo de RLHF
if __name__ == "__main__":
np.random.seed(42)
print("=" * 70)
print("RLHF PIPELINE: REWARD MODEL + PPO")
print("=" * 70)
print()
print("STAGE 1: SFT Model (from Lesson 06)")
print("-" * 40)
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("STAGE 2: Train Reward Model")
print("-" * 40)
rm = RewardModel(
vocab_size=256, embed_dim=128, num_heads=4,
num_layers=4, max_seq_len=128, ff_dim=512
)
rm, rm_losses, rm_accuracies = train_reward_model(rm, PREFERENCE_DATA, num_epochs=10, lr=1e-4)
print()
print("Reward Model Evaluation:")
print("-" * 40)
correct = 0
for pair in PREFERENCE_DATA:
pref_tokens = tokenize_for_reward(pair["prompt"], pair["preferred"])[:128]
rej_tokens = tokenize_for_reward(pair["prompt"], pair["rejected"])[:128]
r_pref = rm.forward(np.array(pref_tokens).reshape(1, -1))[0]
r_rej = rm.forward(np.array(rej_tokens).reshape(1, -1))[0]
if r_pref > r_rej:
correct += 1
print(f" Preferred: {r_pref:+.4f} | Rejected: {r_rej:+.4f} | {'Correct' if r_pref > r_rej else 'Wrong'}")
print(f"\n Accuracy: {correct}/{len(PREFERENCE_DATA)} = {correct/len(PREFERENCE_DATA):.1%}")
print()
print("STAGE 3: PPO Training")
print("-" * 40)
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)
train_prompts = [pair["prompt"] for pair in PREFERENCE_DATA]
policy_model, rewards, kls = ppo_training(
policy_model, reference_model, rm,
train_prompts, num_episodes=20, lr=1.5e-5, kl_coeff=0.02
)
print()
print("=" * 70)
print("COMPARISON: SFT vs RLHF")
print("=" * 70)
print()
eval_prompts = [
"What is the capital of France?",
"Explain gravity.",
"Name three programming languages.",
]
sft_avg, rlhf_avg = compare_models(sft_model, policy_model, rm, eval_prompts)
print()
print("=" * 70)
print("KL DIVERGENCE ANALYSIS")
print("=" * 70)
print()
if kls:
print(f" Initial KL: {kls[0]:.4f}")
print(f" Final KL: {kls[-1]:.4f}")
print(f" Max KL: {max(kls):.4f}")
kl_threshold = 0.1
print(f" KL > {kl_threshold}: {'Yes (model drifted significantly)' if max(kls) > kl_threshold else 'No (model stayed close to reference)'}")
Entregue
Esta lição produz outputs/prompt-reward-model-designer.md — um prompt para projetar pipelines de treinamento de modelos de recompensa. Dado um comportamento alvo (utilidade, capacidade de programação, segurança), ele produz um protocolo de coleta de dados, diretrizes para anotadores e critérios de avaliação do modelo de recompensa.
Exercícios
Modifique o modelo de recompensa para usar a média de todos os estados ocultos em vez de apenas a última posição. Compare a acurácia. A abordagem de pooling de média dá a cada token o mesmo peso, enquanto a abordagem da última posição depende da atenção causal para agregar informações. Teste nos 6 pares de preferência e relate qual abordagem obtém acurácia mais alta.
Implemente a calibração do modelo de recompensa. Após o treinamento, passe todos os pares de preferência pelo modelo de recompensa e compute: (a) a recompensa média para as respostas preferidas, (b) a recompensa média para as respostas rejeitadas, (c) a margem (preferida menos rejeitada). Um modelo bem calibrado deve ter uma margem clara. Em seguida, adicione 4 novos pares de preferência e verifique se a margem se mantém em dados não vistos.
Simule o reward hacking. Crie um modelo de recompensa que dê pontuações altas a respostas longas (reward = len(response) / 100). Execute o PPO com este modelo de recompensa defeituoso e observe o modelo de política gerando saídas cada vez mais longas e repetitivas. Em seguida, adicione uma penalidade KL de 0.1 e mostre que ela previne o comportamento degenerado.
Implemente uma recompensa multi-objetivo. Treine dois modelos de recompensa — um para utilidade (helpfulness) e outro para concisão. Combine-os como R = 0.7 * R_helpful + 0.3 * R_concise. Mostre que o objetivo combinado produz respostas que são tanto úteis quanto concisas, evitando a armadilha da prolixidade de uma única recompensa de utilidade.
Compare diferentes coeficientes KL. Execute o PPO com beta=0.001 (muito baixo, reward hacking), beta=0.02 (padrão) e beta=0.5 (muito alto, sem aprendizado). Plote a curva de recompensa e a curva de KL para cada um. A execução com beta=0.02 deve mostrar uma melhoria constante de recompensa com KL limitada.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| RLHF | "Treinamento com feedback humano" | Reinforcement Learning from Human Feedback: um pipeline de três estágios (SFT, modelo de recompensa, PPO) que otimiza as saídas do modelo de linguagem usando sinais de preferência humana |
| Modelo de recompensa (Reward model) | "Um modelo que avalia respostas" | Um transformer com uma cabeça de saída escalar, treinado em preferências humanas pareadas usando a perda de Bradley-Terry |
| Bradley-Terry | "O modelo de comparação" | Um modelo probabilístico onde P(A > B) = sigmoid(score(A) - score(B)), convertendo preferências pareadas em uma função de pontuação consistente |
| PPO | "O algoritmo de RL" | Proximal Policy Optimization: atualiza a política para maximizar a recompensa ao mesmo tempo que limita (clips) a magnitude da atualização para evitar instabilidade |
| Divergência KL (KL divergence) | "Quão diferentes duas distribuições são" | Uma medida della diferença entre a distribuição de tokens do modelo de política e a do modelo de referência — usada como uma penalidade para evitar o reward hacking |
| Penalidade KL (KL penalty) | "A coleira do modelo" | Beta * KL(política || referência) subtraído do sinal de recompensa — impede que a política divirja muito do checkpoint de SFT |
| Reward hacking | "Burlar a recompensa" | Quando a política encontra saídas degeneradas de alta recompensa ao explorar fraquezas no modelo de recompensa em vez de melhorar genuinamente |
| Par de preferência (Preference pair) | "Qual é melhor, A ou B?" | Um exemplo de treinamento que consiste em (prompt, resposta_preferida, resposta_rejeitada) — a unidade fundamental dos dados de treinamento do RLHF |
| Modelo de referência (Reference model) | "O checkpoint SFT congelado" | Uma cópia do modelo SFT cujos pesos nunca mudam — usada como âncora para o cálculo da divergência KL |
Leituras Adicionais
- Ouyang et al., 2022 -- "Training language models to follow instructions with human feedback" (InstructGPT) — o artigo que tornou o RLHF prático para grandes modelos de linguagem
- Schulman et al., 2017 -- "Proximal Policy Optimization Algorithms" — o artigo original do PPO da OpenAI
- Bai et al., 2022 -- "Training a Helpful and Harmless Assistant with Reinforcement Learning from Human Feedback" — o artigo de RLHF da Anthropic com análise detalhada de reward hacking e penalidade KL
- Stiennon et al., 2020 -- "Learning to summarize with human feedback" — RLHF aplicado à sumarização, mostrando que os modelos de recompensa podem capturar julgamentos de qualidade matizados
- Christiano et al., 2017 -- "Deep reinforcement learning from human preferences" — o trabalho fundacional sobre o aprendizado de funções de recompensa a partir de comparações humanas