RLHF (Reinforcement Learning from Human Feedback) é a técnica que transformou o GPT-3 em ChatGPT — e também o algoritmo mais incompreendido do alinhamento. Não é "treinar com feedback humano" de forma difusa: é uma pipeline RL específica com três etapas (SFT, reward model, PPO), uma loss matemática precisa e um conjunto de patologias bem documentadas. Este módulo entra na matemática de cada componente, com referências aos papers originais.
A pipeline de três estágios
O InstructGPT (Ouyang et al., NeurIPS 2022) cristalizou o pipeline canônico que ainda governa GPT-4, Claude, Llama-Instruct e Gemini em 2026 (com variações). Cada etapa tem um objetivo matemático distinto:
O modelo SFT serve duplo papel: (1) ponto de partida para a policy do PPO (π_θ ← π_SFT) e (2) referência fixa π_ref usada na KL penalty. É a âncora de distribuição.
Stage 2 — Reward Model: aprendendo preferências
O reward model é um classificador escalar: dado (prompt, response), retorna r ∈ ℝ. É treinado via comparações pareadas usando a loss Bradley-Terry — modelagem clássica de preferências (Bradley & Terry, Biometrika 1952) adaptada para LLMs por Christiano et al. 2017 ("Deep RL from Human Preferences", NeurIPS):
O reward model é tipicamente inicializado a partir do SFT (mesma arquitetura, mesmos pesos) com a cabeça LM substituída por uma cabeça linear → ℝ. Para Claude e GPT-4, RMs têm tamanho comparável ao modelo policy. Tamanho do RM importa: RMs pequenos são fáceis de hackear.
# Pseudocódigo do RM training (TRL HuggingFace style)
import torch
import torch.nn.functional as F
def reward_loss(model, batch):
# batch contém pares (chosen, rejected) com mesmo prompt
rewards_chosen = model(batch["input_ids_chosen"]).logits # [B, 1]
rewards_rejected = model(batch["input_ids_rejected"]).logits # [B, 1]
# Bradley-Terry pareada
loss = -F.logsigmoid(rewards_chosen - rewards_rejected).mean()
# Margem de loss accuracy (diagnóstico, não otimizado)
accuracy = (rewards_chosen > rewards_rejected).float().mean()
return loss, accuracyStage 3 — PPO: a loss completa
PPO foi proposto por Schulman et al. 2017 ("Proximal Policy Optimization Algorithms", arxiv.org/abs/1707.06347) para resolver um problema do TRPO: cálculo custoso da trust region via problema quadrático com restrição. PPO substitui a restrição por um clipping na função objetivo — first-order, sem Hessiana, otimizável com Adam.
Em RLHF, a loss total combina o objective PPO com a KL penalty contra a referência SFT e um value function loss para o critic:
O β da KL penalty é o hiperparâmetro mais sensível do RLHF. β baixo (0.001) → policy se afasta muito, reward hacking. β alto (0.5) → policy mal sai do SFT, ganho marginal. InstructGPT usou β=0.02. Implementações modernas usam adaptive KL controller (Ziegler et al. 2019).
Implementação prática: TRL e DeepSpeed-Chat
Em 2026 as duas referências open-source são trl da HuggingFace (github.com/huggingface/trl) e DeepSpeed-Chat da Microsoft. Ambas implementam PPO + KL adaptativo + value head sobre transformers.
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead
from transformers import AutoTokenizer
import torch
# Modelo policy com value head adicionada
policy = AutoModelForCausalLMWithValueHead.from_pretrained("sft-model")
ref_model = AutoModelForCausalLMWithValueHead.from_pretrained("sft-model") # frozen
reward_model = AutoModelForSequenceClassification.from_pretrained("reward-model")
tokenizer = AutoTokenizer.from_pretrained("sft-model")
config = PPOConfig(
model_name="sft-model",
learning_rate=1e-5,
batch_size=128,
mini_batch_size=4,
ppo_epochs=4, # K épocas por batch (clipping importa aqui)
cliprange=0.2, # ε do PPO
cliprange_value=0.2, # clipping do value
vf_coef=0.1, # c_1
init_kl_coef=0.2, # β inicial (adaptativo)
target_kl=6.0, # alvo do KL controller
gamma=1.0,
lam=0.95, # λ do GAE
)
trainer = PPOTrainer(config, policy, ref_model, tokenizer)
for batch in dataloader:
# 1. Sample da policy
query_tensors = batch["input_ids"]
response_tensors = trainer.generate(query_tensors, max_new_tokens=200)
# 2. Compute rewards via reward model
texts = [tokenizer.decode(r) for r in response_tensors]
rewards = [reward_model(t).logits[0] for t in texts]
# 3. PPO step (computa advantage via GAE, aplica clipping, KL penalty)
stats = trainer.step(query_tensors, response_tensors, rewards)
print(f"kl={stats['objective/kl']:.3f} reward={stats['ppo/mean_scores']:.3f}")Reward hacking: a lei de Goodhart aplicada
"When a measure becomes a target, it ceases to be a good measure" (Charles Goodhart, 1975). O reward model é um proxy do julgamento humano — não o julgamento em si. Quando a policy tem capacidade de otimização suficiente, encontra regiões de alto reward que humanos não validariam. Skalse et al. 2022 ("Defining and Characterizing Reward Hacking", NeurIPS) formalizou o fenômeno.
| Sintoma observado | Causa provável | Mitigação |
|---|---|---|
| Respostas excessivamente longas | RM correlaciona length com qualidade | Length normalization no reward |
| Sycophancy ("você está certo!") | Raters humanos preferem concordância | Diversidade de raters, contrastive data |
| Hedging exagerado ("pode ser que...") | RM premia respostas defensivas | KL alta + curadoria de raters |
| Repetição de keywords ("vamos analisar") | Pattern de open-class no RM | RM ensembles com voting |
| Recusa excessiva | Harmlessness RM domina helpfulness RM | Tradeoff explícito (Constitutional AI) |
📋 Você observa que reward aumenta ao longo do treino mas eval humano piora — clássico do reward hacking começando.
A divergência entre proxy (RM) e objetivo (humano) é o sinal definitivo de hacking. KL maior ancora a policy ao SFT. Eval humano em loop pega o problema antes do modelo ficar não-recuperável.
Alt: Reduzir learning rate —
Alt: Ensemble de RMs —
Linha do tempo do RLHF
Arquitetura de runtime no PPO step
Perguntas frequentes
❓ Por que PPO e não Q-learning ou DDPG?
❓ Por que 4 épocas de PPO por batch?