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 (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
- 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.
- 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.
- Difícil. Faça fine-tuning do
facebook/bart-baseem 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
- Sutskever, Vinyals, Le (2014). Sequence to Sequence Learning with Neural Networks — o artigo seq2seq original. Quatro páginas.
- Cho et al. (2014). Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation — introduziu a GRU e o enquadramento codificador-decodificador.
- Bahdanau, Cho, Bengio (2014). Neural Machine Translation by Jointly Learning to Align and Translate — o artigo da atenção. Leia logo depois desta lição.
- Tutorial PyTorch NLP from Scratch — código de seq2seq + atenção que dá para construir.