Phase 05 - Lesson 08
CNNs e RNNs para Texto
Convoluções aprendem n-gramas. Recorrências lembram. Ambas foram superadas por atenção. Ambas ainda importam em hardware limitado.
Tipo: Construir Idiomas: Python Pré-requisitos: Fase 3 · 11 (PyTorch Intro), Fase 5 · 03 (Word Embeddings), Fase 4 · 02 (Convolutions from Scratch) Tempo: ~75 minutos
O Problema
TF-IDF e Word2Vec produziam vetores planos que ignoravam a ordem das palavras. Um classificador construído sobre eles não conseguiria diferenciar dog bites man de man bites dog. A ordem das palavras às vezes carrega o sinal.
Duas famílias de arquiteturas preencheram essa lacuna antes da chegada dos transformers.
Redes convolucionais para texto (TextCNN). Aplicam convoluções 1D sobre sequências de embeddings de palavras. Um filtro de largura 3 é um detector de trigramas aprendível: ele abrange três palavras e gera uma pontuação. Empilhe larguras diferentes (2, 3, 4, 5) para detectar padrões em múltiplas escalas. Faça max-pool para obter uma representação de tamanho fixo. Plano, paralelo, rápido.
Redes recorrentes (RNN, LSTM, GRU). Processam tokens um por vez, mantendo um estado oculto (hidden state) que carrega informações adiante. Sequencial, com memória, comprimentos de entrada flexíveis. Dominaram a modelagem de sequências de 2014 a 2017, até que a atenção surgiu.
Esta lição constrói ambas, e depois aponta a falha que motivou o mecanismo de atenção.
O Conceito
TextCNN (Kim, 2014). Os tokens são incorporados (embedded). Uma convolução 1D de largura k desliza um filtro sobre k-gramas consecutivos de embeddings, produzindo um mapa de características (feature map). O max-pooling global sobre esse mapa seleciona a ativação mais forte. Concatene as saídas de max-pooling de várias larguras de filtros. Alimente uma cabeça classificadora (classifier head).
Por que funciona. Um filtro é um n-grama aprendível. O max-pooling é invariante à posição, de modo que "not good" ativa a mesma característica no início ou no meio de uma avaliação. Três larguras de filtro com 100 filtros cada fornecem 300 detectores de n-gramas aprendidos. O treinamento é paralelo; não há dependência sequencial.
RNN. Em cada etapa de tempo t, o estado oculto h_t = f(W * x_t + U * h_{t-1} + b). Compartilhe W, U, b ao longo do tempo. O estado oculto no momento T é um resumo de todo o prefixo. Para classificação, faça pooling ao longo de h_1 ... h_T (max, mean ou last).
RNNs puras sofrem com o desaparecimento do gradiente (vanishing gradients). A LSTM adiciona portas (gates) que decidem o que esquecer, o que armazenar e o que produzir como saída, estabilizando os gradientes em sequências longas. O GRU simplifica a LSTM para duas portas; apresenta desempenho semelhante com menos parâmetros.
RNNs Bidirecionais executam uma RNN para frente e outra para trás, concatenando os estados ocultos. A representação de cada token vê tanto o contexto esquerdo quanto o direito. Essencial para tarefas de marcação (tagging).
Construa
Passo 1: TextCNN no PyTorch
import torch
import torch.nn as nn
import torch.nn.functional as F
class TextCNN(nn.Module):
def __init__(self, vocab_size, embed_dim, n_classes, filter_widths=(2, 3, 4), n_filters=64, dropout=0.3):
super().__init__()
self.embed = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
self.convs = nn.ModuleList([
nn.Conv1d(embed_dim, n_filters, kernel_size=k)
for k in filter_widths
])
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(n_filters * len(filter_widths), n_classes)
def forward(self, token_ids):
x = self.embed(token_ids).transpose(1, 2)
pooled = []
for conv in self.convs:
c = F.relu(conv(x))
p = F.max_pool1d(c, c.size(2)).squeeze(2)
pooled.append(p)
h = torch.cat(pooled, dim=1)
return self.fc(self.dropout(h))
O transpose(1, 2) reformata [batch, seq_len, embed_dim] para [batch, embed_dim, seq_len] porque nn.Conv1d trata o eixo central como canais. A saída resultante do pooling tem tamanho fixo, independentemente do comprimento da entrada.
Passo 2: Classificador LSTM
class LSTMClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, n_classes, bidirectional=True, dropout=0.3):
super().__init__()
self.embed = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True, bidirectional=bidirectional)
factor = 2 if bidirectional else 1
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(hidden_dim * factor, n_classes)
def forward(self, token_ids):
x = self.embed(token_ids)
out, _ = self.lstm(x)
pooled = out.max(dim=1).values
return self.fc(self.dropout(pooled))
Faça max-pool sobre a sequência, e não pooling de estado final (last-state). Para classificação, o max-pooling geralmente supera o uso do último estado oculto, porque as informações no final de uma sequência longa tendem a dominar o último estado.
Passo 3: Demonstração do desaparecimento do gradiente (intuição)
Uma RNN simples sem portas não consegue aprender dependências de longo alcance. Considere uma tarefa fictícia: prever se o token A apareceu em qualquer lugar de uma sequência. Se A estiver na posição 1 e a sequência tiver 100 tokens de comprimento, o gradiente da perda terá que fluir de volta por meio de 99 multiplicações do peso recorrente. Se o peso for menor que 1, o gradiente desaparece (vanishes). Se for maior que 1, ele explode (explodes).
def vanishing_gradient_sim(seq_len, recurrent_weight=0.9):
import math
return math.pow(recurrent_weight, seq_len)
# At weight=0.9 over 100 steps:
# 0.9 ^ 100 ≈ 2.7e-5
# The gradient from step 100 to step 1 is effectively zero.
As LSTMs corrigem isso com um estado de célula (cell state) que percorre a rede apenas com interações aditivas (a porta de esquecimento o dimensiona multiplicativamente, mas os gradientes ainda fluem pela "via expressa"). As GRUs fazem algo semelhante com menos parâmetros. Ambas proporcionam um treinamento estável em sequências de mais de 100 etapas.
Passo 4: Por que isso ainda não era suficiente
Três problemas persistiram mesmo com as LSTMs.
- Gargalo sequencial. Treinar uma RNN em uma sequência de comprimento 1000 requer 1000 etapas seriais de avanço/retrocesso (forward/backward). Não é possível paralelizar ao longo do tempo.
- Vetor de contexto de tamanho fixo em configurações codificador-decodificador (encoder-decoder). O decodificador vê apenas o estado oculto final do codificador, compactado sobre toda a entrada. Entradas longas perdem detalhes. A Lição 09 aborda isso diretamente.
- Teto de precisão para dependências distantes. As LSTMs superam as RNNs simples, mas ainda lutam para propagar informações específicas por mais de 200 etapas.
A atenção resolveu os três problemas. Os transformers abandonaram a recorrência por completo. A Lição 10 é o ponto de virada.
Como usar
Os módulos nn.LSTM, nn.GRU e nn.Conv1d do PyTorch estão prontos para produção. O código de treinamento é padrão.
O Hugging Face disponibiliza embeddings pré-treinados que você pode conectar como a camada de entrada:
from transformers import AutoModel
encoder = AutoModel.from_pretrained("bert-base-uncased")
for param in encoder.parameters():
param.requires_grad = False
class BertCNN(nn.Module):
def __init__(self, n_classes, filter_widths=(2, 3, 4), n_filters=64):
super().__init__()
self.encoder = encoder
self.convs = nn.ModuleList([nn.Conv1d(768, n_filters, kernel_size=k) for k in filter_widths])
self.fc = nn.Linear(n_filters * len(filter_widths), n_classes)
def forward(self, input_ids, attention_mask):
with torch.no_grad():
out = self.encoder(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
x = out.transpose(1, 2)
pooled = [F.max_pool1d(F.relu(conv(x)), kernel_size=conv(x).size(2)).squeeze(2) for conv in self.convs]
return self.fc(torch.cat(pooled, dim=1))
Lista de verificação para "usar quando couber na restrição":
- Inferência na borda (edge) / no dispositivo. TextCNN com embeddings GloVe é de 10 a 100 vezes menor do que um transformer. Se o seu alvo de implantação (deploy) for um celular, essa é a pilha (stack).
- Classificação por streaming / online. A RNN processa um token de cada vez; os transformers precisam da sequência completa. Para textos que chegam em tempo real, as LSTMs ainda vencem.
- Modelos minúsculos para linhas de base (baselines). Iteração rápida em uma nova tarefa. Treine uma TextCNN em 5 minutos em uma CPU.
- Marcação de sequência com dados limitados. BiLSTM-CRF (lição 06) ainda é uma arquitetura de NER de nível de produção para 1k a 10k sentenças rotuladas.
Tudo o mais vai para um transformer.
Envie
Salve como outputs/prompt-text-encoder-picker.md:
---
name: text-encoder-picker
description: Pick a text encoder architecture for a given constraint set.
phase: 5
lesson: 08
---
Given constraints (task, data volume, latency budget, deploy target, compute budget), output:
1. Encoder architecture: TextCNN, BiLSTM, BiLSTM-CRF, transformer fine-tune, or "use a pretrained transformer as a frozen encoder + small head".
2. Embedding input: random init, GloVe / fastText frozen, or contextualized transformer embeddings.
3. Training recipe in 5 lines: optimizer, learning rate, batch size, epochs, regularization.
4. One monitoring signal. For RNN/CNN models: attention mechanism absence means they miss long-range deps; check per-length accuracy. For transformers: fine-tuning collapse if LR too high; check train loss.
Refuse to recommend fine-tuning a transformer when data is under ~500 labeled examples without showing that a TextCNN / BiLSTM baseline has plateaued. Flag edge deployment as needing architecture-before-everything.
Exercícios
- Fácil. Treine uma TextCNN em um conjunto de dados fictício de 3 classes (você inventa os dados). Verifique se as larguras de filtro (2, 3, 4) superam uma única largura (3) no F1 médio.
- Médio. Implemente max-pool, mean-pool e pooling de último estado (last-state) para o classificador LSTM. Compare em um conjunto de dados pequeno; documente qual pooling vence e elabore uma hipótese do porquê.
- Difícil. Construa um marcador NER BiLSTM-CRF (combine a lição 06 e esta). Treine no CoNLL-2003. Compare com a linha de base apenas com CRF da lição 06 e com um ajuste fino (fine-tune) do BERT. Relate o tempo de treinamento, a memória e o F1.
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| TextCNN | CNN para texto | Pilha de convoluções 1D sobre embeddings de palavras com max-pool global. Kim (2014). |
| RNN | Rede recorrente | Estado oculto atualizado a cada etapa de tempo: h_t = f(W x_t + U h_{t-1}). |
| LSTM | RNN com portas | Adiciona portas de entrada / esquecimento / saída + um estado de célula. Treina de forma estável em sequências longas. |
| GRU | LSTM mais simples | Duas portas em vez de três. Precisão semelhante, menos parâmetros. |
| Bidirecional | Ambas as direções | RNN direta + reversa concatenadas. Cada token vê ambos os lados de seu contexto. |
| Desaparecimento do gradiente | O sinal de treinamento morre | Multiplicações repetidas por pesos <1 em RNNs puras fazem com que os gradientes das etapas iniciais sejam efetivamente zero. |
Leitura Adicional
- Kim, Y. (2014). Convolutional Neural Networks for Sentence Classification — o artigo do TextCNN. Oito páginas. Leitura acessível.
- Hochreiter, S. and Schmidhuber, J. (1997). Long Short-Term Memory — o artigo da LSTM. Inesperadamente lúcido.
- Olah, C. (2015). Understanding LSTM Networks — os diagramas que tornaram as LSTMs acessíveis a todos.