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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

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