Phase 05 - Lesson 09

Modelos Sequência-para-Sequência

Duas RNNs fingindo ser um tradutor. O gargalo que elas enfrentam é a razão pela qual a atenção existe.

Tipo: Build Linguagens: Python Pré-requisitos: Fase 5 · 08 (CNNs + RNNs para Texto), Fase 3 · 11 (Introdução ao PyTorch) Tempo: ~75 minutos

O Problema

A classificação mapeia uma sequência de comprimento variável para um único rótulo. A tradução mapeia uma sequência de comprimento variável para outra sequência de comprimento variável. A entrada e a saída vivem em vocabulários diferentes, possivelmente idiomas diferentes, sem nenhuma garantia de paridade de comprimento.

A arquitetura seq2seq (Sutskever, Vinyals, Le, 2014) resolveu isso com uma receita deliberadamente simples. Duas RNNs. Uma lê a frase de origem e produz um vetor de contexto de tamanho fixo. A outra lê esse vetor e gera a frase de destino token por token. O mesmo código que você escreveu na lição 08, colado de forma diferente.

Vale a pena estudar isso por dois motivos. Primeiro, o gargalo do vetor de contexto é a falha mais pedagogicamente útil em PLN. Ele motiva tudo aquilo em que a atenção e os transformers são bons. Segundo, a receita de treinamento (teacher forcing, scheduled sampling, beam search na inferência) ainda se aplica a todo sistema de geração moderno, incluindo os LLMs.

O Conceito

Codificador-decodificador com gargalo do vetor de contexto

Codificador (encoder). Uma RNN que lê a frase de origem. Seu estado oculto final é o vetor de contexto — um resumo de tamanho fixo de toda a entrada. Sem perder nada além da origem, supostamente.

Decodificador (decoder). Outra RNN inicializada a partir do vetor de contexto. A cada passo, ela recebe o token gerado anteriormente como entrada e produz uma distribuição sobre o vocabulário de destino. Faça amostragem ou argmax para escolher o próximo token. Realimente-o. Repita até que um token <EOS> seja produzido ou o comprimento máximo seja atingido.

Treinamento: perda de entropia cruzada em cada passo do decodificador, somada ao longo da sequência. Backpropagation padrão através do tempo, em ambas as redes.

Teacher forcing. Durante o treinamento, a entrada do decodificador no passo t é o token verdadeiro (ground-truth) na posição t-1, não a própria predição anterior do decodificador. Isso estabiliza o treinamento; sem isso, erros iniciais se propagam em cascata e o modelo nunca aprende. Na inferência, você precisa usar as próprias predições do modelo, então sempre há uma lacuna de distribuição entre treinamento e inferência. Essa lacuna é chamada de exposure bias (viés de exposição).

O gargalo. Tudo o que o codificador aprendeu sobre a origem precisa ser espremido naquele único vetor de contexto. Frases longas perdem detalhes. Palavras raras ficam borradas. Reordenação (chat noir vs. black cat) tem que ser memorizada, não computada.

A atenção (lição 10) corrige isso permitindo que o decodificador olhe para cada estado oculto do codificador, não apenas para o último. Esse é o argumento todo.

Construa

Passo 1: um codificador

import torch
import torch.nn as nn


class Encoder(nn.Module):
    def __init__(self, src_vocab_size, embed_dim, hidden_dim):
        super().__init__()
        self.embed = nn.Embedding(src_vocab_size, embed_dim, padding_idx=0)
        self.gru = nn.GRU(embed_dim, hidden_dim, batch_first=True)

    def forward(self, src):
        e = self.embed(src)
        outputs, hidden = self.gru(e)
        return outputs, hidden

outputs tem formato [batch, seq_len, hidden_dim] — um estado oculto por posição de entrada. hidden tem formato [1, batch, hidden_dim] — o passo final. A lição 08 dizia "faça pooling sobre os outputs para classificação". Aqui mantemos o último estado oculto como o vetor de contexto e ignoramos os outputs por passo.

Passo 2: um decodificador

class Decoder(nn.Module):
    def __init__(self, tgt_vocab_size, embed_dim, hidden_dim):
        super().__init__()
        self.embed = nn.Embedding(tgt_vocab_size, embed_dim, padding_idx=0)
        self.gru = nn.GRU(embed_dim, hidden_dim, batch_first=True)
        self.fc = nn.Linear(hidden_dim, tgt_vocab_size)

    def forward(self, token, hidden):
        e = self.embed(token)
        out, hidden = self.gru(e, hidden)
        logits = self.fc(out)
        return logits, hidden

O decodificador é chamado um passo de cada vez. Entrada: um lote de tokens individuais e o estado oculto atual. Saída: logits do vocabulário para o próximo token e o estado oculto atualizado.

Passo 3: laço de treinamento com teacher forcing

def train_batch(encoder, decoder, src, tgt, bos_id, optimizer, teacher_forcing_ratio=0.9):
    optimizer.zero_grad()
    _, hidden = encoder(src)
    batch_size, tgt_len = tgt.shape
    input_token = torch.full((batch_size, 1), bos_id, dtype=torch.long)
    loss = 0.0
    loss_fn = nn.CrossEntropyLoss(ignore_index=0)

    for t in range(tgt_len):
        logits, hidden = decoder(input_token, hidden)
        step_loss = loss_fn(logits.squeeze(1), tgt[:, t])
        loss += step_loss
        use_teacher = torch.rand(1).item() < teacher_forcing_ratio
        if use_teacher:
            input_token = tgt[:, t].unsqueeze(1)
        else:
            input_token = logits.argmax(dim=-1)

    loss.backward()
    optimizer.step()
    return loss.item() / tgt_len

Dois botões que vale a pena nomear. ignore_index=0 ignora a perda nos tokens de preenchimento (padding). teacher_forcing_ratio é a probabilidade de usar o token verdadeiro em vez da predição do modelo a cada passo. Comece em 1.0 (teacher forcing total) e reduza gradualmente até ~0.5 ao longo do treinamento para fechar a lacuna do exposure bias.

Passo 4: laço de inferência (greedy)

@torch.no_grad()
def greedy_decode(encoder, decoder, src, bos_id, eos_id, max_len=50):
    _, hidden = encoder(src)
    batch_size = src.shape[0]
    input_token = torch.full((batch_size, 1), bos_id, dtype=torch.long)
    output_ids = []
    for _ in range(max_len):
        logits, hidden = decoder(input_token, hidden)
        next_token = logits.argmax(dim=-1)
        output_ids.append(next_token)
        input_token = next_token
        if (next_token == eos_id).all():
            break
    return torch.cat(output_ids, dim=1)

A decodificação greedy escolhe o token de maior probabilidade a cada passo. Ela pode se desviar: uma vez que você se compromete com um token, não dá para desfazer. O beam search mantém vivas as k sequências parciais de maior pontuação e escolhe a sequência completa de maior pontuação no final. Largura de feixe (beam width) de 3 a 5 é o padrão.

Passo 5: o gargalo, demonstrado

Treine o modelo em uma tarefa-brinquedo de cópia: origem [a, b, c, d, e], destino [a, b, c, d, e]. Aumente o comprimento da sequência. Observe a acurácia.

seq_len=5   copy accuracy: 98%
seq_len=10  copy accuracy: 91%
seq_len=20  copy accuracy: 62%
seq_len=40  copy accuracy: 23%

Um único estado oculto de GRU não consegue memorizar sem perdas uma entrada de 40 tokens. A informação está lá em cada passo do codificador, mas o decodificador só enxerga o último estado. A atenção corrige isso diretamente.

Use

O PyTorch tem o nn.Transformer e templates de seq2seq baseados em nn.LSTM. A biblioteca transformers da Hugging Face fornece modelos codificador-decodificador completos (BART, T5, mBART, NLLB) treinados em bilhões de tokens.

from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

tok = AutoTokenizer.from_pretrained("facebook/bart-base")
model = AutoModelForSeq2SeqLM.from_pretrained("facebook/bart-base")

src = tok("Translate this to French: Hello, how are you?", return_tensors="pt")
out = model.generate(**src, max_new_tokens=50, num_beams=4)
print(tok.decode(out[0], skip_special_tokens=True))

Os codificadores-decodificadores modernos abandonaram as RNNs em favor dos transformers. O formato de alto nível (codificador, decodificador, gerar token por token) é idêntico ao do artigo seq2seq de 2014. O mecanismo dentro de cada bloco é diferente.

Quando ainda recorrer ao seq2seq baseado em RNN

Quase nunca, para projetos novos. Exceções específicas:

  • Tradução por streaming, onde você consome a entrada um token de cada vez com memória limitada.
  • Geração de texto no dispositivo (on-device), onde o custo de memória do transformer é proibitivo.
  • Pedagogia. Entender o gargalo do codificador-decodificador é o caminho mais rápido para entender por que os transformers venceram.

Exposure bias e suas mitigações

  • Scheduled sampling. Reduza gradualmente a razão de teacher forcing durante o treinamento para que o modelo aprenda a se recuperar dos próprios erros.
  • Treinamento de risco mínimo (minimum risk training). Treine sobre o score BLEU em nível de frase em vez da entropia cruzada em nível de token. Mais próximo do que você realmente quer.
  • Fine-tuning por aprendizado por reforço. Recompense o gerador de sequências com uma métrica. Usado no RLHF dos LLMs modernos.

Os três ainda se aplicam à geração baseada em transformers.

Entregue

Salve como outputs/prompt-seq2seq-design.md:

---
name: seq2seq-design
description: Design a sequence-to-sequence pipeline for a given task.
phase: 5
lesson: 09
---

Given a task (translation, summarization, paraphrase, question rewrite), output:

1. Architecture. Pretrained transformer encoder-decoder (BART, T5, mBART, NLLB) is the default. RNN-based seq2seq only for specific constraints.
2. Starting checkpoint. Name it (`facebook/bart-base`, `google/flan-t5-base`, `facebook/nllb-200-distilled-600M`). Match the checkpoint to task and language coverage.
3. Decoding strategy. Greedy for deterministic output, beam search (width 4-5) for quality, sampling with temperature for diversity. One sentence justification.
4. One failure mode to verify before shipping. Exposure bias manifests as generation drift on longer outputs; sample 20 outputs at the 90th-percentile length and eyeball.

Refuse to recommend training a seq2seq from scratch for under a million parallel examples. Flag any pipeline that uses greedy decoding for user-facing content as fragile (greedy repeats and loops).

Exercícios

  1. Fácil. Implemente a tarefa-brinquedo de cópia. Treine um seq2seq com GRU em pares de entrada-saída onde o destino é igual à origem. Meça a acurácia nos comprimentos 5, 10, 20. Reproduza o gargalo.
  2. Médio. Adicione decodificação por beam search com largura de feixe 3. Meça o BLEU em um pequeno corpus paralelo em comparação ao greedy. Documente onde o beam search vence (geralmente nos últimos tokens) e onde ele não faz diferença.
  3. Difícil. Faça fine-tuning do facebook/bart-base em um conjunto de dados de paráfrase com 10 mil pares. Compare a saída beam-4 do modelo ajustado com a do modelo base em entradas reservadas (held-out). Reporte o BLEU e escolha 10 exemplos qualitativos.

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
Codificador (encoder) RNN de entrada Lê a origem. Produz estados ocultos por passo e um vetor de contexto final.
Decodificador (decoder) RNN de saída Inicializado a partir do vetor de contexto. Gera tokens de destino um de cada vez.
Vetor de contexto O resumo Estado oculto final do codificador. Tamanho fixo. O gargalo que a atenção resolve.
Teacher forcing Usar tokens verdadeiros Alimenta o token anterior verdadeiro durante o treinamento. Estabiliza o aprendizado.
Exposure bias Lacuna treino/teste Modelo treinado com tokens verdadeiros nunca praticou se recuperar dos próprios erros.
Beam search Decodificação melhor Mantém vivas as top-k sequências parciais a cada passo em vez de se comprometer de forma greedy.

Leitura Adicional

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