Objetivo
Entender como texto é transformado em representações numéricas que modelos de ML conseguem processar. Dominar desde representações simples (Bag of Words, TF-IDF) até embeddings densos (Word2Vec, GloVe) e embeddings contextuais modernos. Esta aula inicia a Fase 5 — IA Generativa e Transformers.
1. O Problema Fundamental: Texto para Números
Modelos de ML trabalham com números. Texto é discreto, simbólico e sem ordem natural numérica. Como transformar "o gato sentou no tapete" em algo que uma rede neural possa processar?
A evolução histórica das representações:
1990s: One-Hot Encoding → simples, sem semântica
2000s: Bag of Words / TF-IDF → estatístico, sem contexto
2013: Word2Vec → denso, semântico, estático
2014: GloVe → denso, semântico, global
2018: ELMo → contextual, bidirecional
2018+: BERT, GPT → contextual profundo
2. Representações Clássicas
2.1 One-Hot Encoding
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from collections import Counter, defaultdict
from sklearn.metrics.pairwise import cosine_similarity
import warnings
warnings.filterwarnings("ignore")
np.random.seed(42)
torch.manual_seed(42)
sns.set_theme(style="whitegrid")
# Corpus simples
corpus = [
"o gato sentou no tapete",
"o cachorro correu no parque",
"o gato e o cachorro brincaram",
"o tapete é vermelho",
"o parque é verde",
]
# Construir vocabulário
todas_palavras = " ".join(corpus).split()
vocab = sorted(set(todas_palavras))
palavra_para_idx = {p: i for i, p in enumerate(vocab)}
idx_para_palavra = {i: p for p, i in palavra_para_idx.items()}
print(f"Vocabulário ({len(vocab)} palavras):")
print(f" {vocab}\n")
# One-hot encoding
def one_hot(palavra: str, vocab: dict) -> np.ndarray:
v = np.zeros(len(vocab))
v[vocab[palavra]] = 1.0
return v
# Exemplos
for palavra in ["gato", "cachorro", "tapete"]:
oh = one_hot(palavra, palavra_para_idx)
print(f" '{palavra}': {oh.astype(int)}")
print(f"\nProblemas do One-Hot:")
print(f" 1. Dimensão = tamanho do vocab (pode ser 50.000+)")
print(f" 2. Vetores esparsos — 99.99% zeros")
print(f" 3. Sem semântica: sim(gato, cachorro) = "
f"{np.dot(one_hot('gato', palavra_para_idx), one_hot('cachorro', palavra_para_idx)):.1f}")
print(f" 4. Sem semântica: sim(gato, tapete) = "
f"{np.dot(one_hot('gato', palavra_para_idx), one_hot('tapete', palavra_para_idx)):.1f}")
print(f" Gato e cachorro são tão 'distantes' quanto gato e tapete!")
2.2 Bag of Words (BoW)
def bag_of_words(texto: str, vocab: dict) -> np.ndarray:
"""Conta frequência de cada palavra no texto."""
v = np.zeros(len(vocab))
for palavra in texto.split():
if palavra in vocab:
v[vocab[palavra]] += 1
return v
print("\nBag of Words:")
for texto in corpus:
bow = bag_of_words(texto, palavra_para_idx)
print(f" '{texto}'")
# Mostrar apenas palavras presentes
presentes = {idx_para_palavra[i]: int(bow[i])
for i in range(len(vocab)) if bow[i] > 0}
print(f" → {presentes}")
# Matriz documento-termo
matriz_bow = np.array([
bag_of_words(t, palavra_para_idx) for t in corpus
])
print(f"\nMatriz documento-termo: {matriz_bow.shape}")
print(f" {len(corpus)} documentos × {len(vocab)} palavras")
2.3 TF-IDF
def calcular_tfidf(corpus: list) -> tuple:
"""
Calcula TF-IDF manualmente.
TF(t, d) = freq(t, d) / total_termos(d)
IDF(t) = log(N / df(t))
TF-IDF(t, d) = TF(t, d) × IDF(t)
"""
# Construir vocabulário
vocab_tfidf = sorted(set(
" ".join(corpus).split()
))
v2i = {p: i for i, p in enumerate(vocab_tfidf)}
N = len(corpus)
# TF por documento
tf_matrix = np.zeros((N, len(vocab_tfidf)))
for d_idx, doc in enumerate(corpus):
palavras = doc.split()
total = len(palavras)
for p in palavras:
if p in v2i:
tf_matrix[d_idx, v2i[p]] += 1 / total
# DF (document frequency) por termo
df = np.zeros(len(vocab_tfidf))
for doc in corpus:
presentes = set(doc.split())
for p in presentes:
if p in v2i:
df[v2i[p]] += 1
# IDF
idf = np.log(N / (df + 1)) # +1 evita divisão por zero
# TF-IDF
tfidf_matrix = tf_matrix * idf[np.newaxis, :]
return tfidf_matrix, vocab_tfidf, idf
tfidf_matrix, vocab_tfidf, idf_vals = calcular_tfidf(corpus)
print("TF-IDF:")
print(f" Matriz: {tfidf_matrix.shape}")
# Palavras com maior IDF (mais informativas)
idx_sorted = np.argsort(idf_vals)[::-1]
print(f"\n Palavras mais informativas (maior IDF):")
for i in idx_sorted[:5]:
print(f" '{vocab_tfidf[i]}': IDF={idf_vals[i]:.4f}")
print(f"\n Palavras menos informativas (menor IDF):")
for i in idx_sorted[-3:]:
print(f" '{vocab_tfidf[i]}': IDF={idf_vals[i]:.4f}")
# Visualizar matriz TF-IDF
fig, axes = plt.subplots(1, 2, figsize=(16, 5))
im1 = axes[0].imshow(tfidf_matrix, cmap="YlOrRd",
aspect="auto")
axes[0].set_xticks(range(len(vocab_tfidf)))
axes[0].set_xticklabels(vocab_tfidf, rotation=45,
ha="right", fontsize=8)
axes[0].set_yticks(range(len(corpus)))
axes[0].set_yticklabels(
[f"Doc {i}" for i in range(len(corpus))],
fontsize=9
)
axes[0].set_title("Matriz TF-IDF")
plt.colorbar(im1, ax=axes[0])
# Similaridade entre documentos
sim_matrix = cosine_similarity(tfidf_matrix)
im2 = axes[1].imshow(sim_matrix, cmap="Blues",
vmin=0, vmax=1)
axes[1].set_xticks(range(len(corpus)))
axes[1].set_yticks(range(len(corpus)))
labels_doc = [f"Doc {i}" for i in range(len(corpus))]
axes[1].set_xticklabels(labels_doc, fontsize=9)
axes[1].set_yticklabels(labels_doc, fontsize=9)
for i in range(len(corpus)):
for j in range(len(corpus)):
axes[1].text(j, i, f"{sim_matrix[i,j]:.2f}",
ha="center", va="center",
fontsize=9)
axes[1].set_title("Similaridade Cosseno (TF-IDF)")
plt.colorbar(im2, ax=axes[1])
plt.suptitle("TF-IDF: Representação Clássica de Texto",
fontsize=13)
plt.tight_layout()
plt.show()
3. Word2Vec — Embeddings Densos
Word2Vec (Mikolov et al., 2013) aprende embeddings densos onde a similaridade geométrica captura semântica.
A ideia central: palavras que aparecem em contextos similares têm significados similares (Hipótese Distribucional).
Duas arquiteturas: - CBOW (Continuous Bag of Words): prevê palavra central dado contexto - Skip-gram: prevê contexto dado palavra central
class SkipGram(nn.Module):
"""
Implementação do Skip-Gram (Word2Vec).
Aprende embeddings prevendo palavras de contexto.
"""
def __init__(self, vocab_size: int,
embed_dim: int = 10):
super().__init__()
# Embedding de entrada (palavras centrais)
self.entrada = nn.Embedding(vocab_size, embed_dim)
# Embedding de saída (palavras de contexto)
self.saida = nn.Embedding(vocab_size, embed_dim)
# Inicialização uniforme pequena
nn.init.uniform_(self.entrada.weight, -0.1, 0.1)
nn.init.zeros_(self.saida.weight)
def forward(self, centro: torch.Tensor,
contexto: torch.Tensor) -> torch.Tensor:
"""
centro: (batch,) índices das palavras centrais
contexto: (batch,) índices das palavras de contexto
"""
v_centro = self.entrada(centro) # (batch, embed_dim)
v_contexto = self.saida(contexto) # (batch, embed_dim)
# Produto escalar → similaridade
score = (v_centro * v_contexto).sum(dim=1)
return score
def get_embedding(self, idx: int) -> np.ndarray:
with torch.no_grad():
return self.entrada(
torch.tensor(idx)
).numpy()
def gerar_pares_skipgram(corpus: list,
palavra_para_idx: dict,
janela: int = 2) -> list:
"""
Gera pares (centro, contexto) para treino do Skip-Gram.
"""
pares = []
for frase in corpus:
tokens = [palavra_para_idx[p]
for p in frase.split()
if p in palavra_para_idx]
for i, centro in enumerate(tokens):
inicio = max(0, i - janela)
fim = min(len(tokens), i + janela + 1)
for j in range(inicio, fim):
if j != i:
pares.append((centro, tokens[j]))
return pares
# Corpus maior para treinar Word2Vec
corpus_w2v = [
"o gato é um animal doméstico",
"o cachorro é um animal doméstico",
"gatos e cachorros são animais",
"o gato mia e o cachorro late",
"animais domésticos incluem gatos e cachorros",
"o leão é um felino selvagem",
"o tigre é um felino selvagem",
"felinos incluem gatos leões e tigres",
"o lobo é um canídeo selvagem",
"cachorros e lobos são canídeos",
"o oceano é azul e profundo",
"o mar tem ondas e peixes",
"peixes vivem no oceano e no mar",
"a praia fica perto do oceano",
"o sol brilha no céu azul",
"as nuvens estão no céu",
"o vento sopra nas árvores",
"a floresta tem muitas árvores",
"animais vivem na floresta",
"gatos caçam na floresta e em casa",
]
# Construir vocabulário do corpus maior
todas_w = " ".join(corpus_w2v).split()
vocab_w2v = sorted(set(todas_w))
p2i_w2v = {p: i for i, p in enumerate(vocab_w2v)}
i2p_w2v = {i: p for p, i in p2i_w2v.items()}
pares = gerar_pares_skipgram(corpus_w2v, p2i_w2v, janela=2)
print(f"Word2Vec Skip-Gram:")
print(f" Vocabulário: {len(vocab_w2v)} palavras")
print(f" Pares (centro, contexto): {len(pares)}")
# Treinar Skip-Gram com Negative Sampling simplificado
modelo_w2v = SkipGram(
vocab_size=len(vocab_w2v),
embed_dim=16
)
optimizer_w2v = torch.optim.Adam(
modelo_w2v.parameters(), lr=0.01
)
criterion_w2v = nn.BCEWithLogitsLoss()
# Converter pares para tensores
centros_t = torch.tensor([p[0] for p in pares])
contextos_t = torch.tensor([p[1] for p in pares])
n_pares = len(pares)
losses_w2v = []
print(f"\nTreinando Skip-Gram por 200 épocas...")
for epoca in range(200):
# Pares positivos
scores_pos = modelo_w2v(centros_t, contextos_t)
labels_pos = torch.ones(n_pares)
# Negative sampling: amostrar palavras aleatórias
negatives = torch.randint(0, len(vocab_w2v),
(n_pares,))
scores_neg = modelo_w2v(centros_t, negatives)
labels_neg = torch.zeros(n_pares)
# Loss combinada
scores = torch.cat([scores_pos, scores_neg])
labels = torch.cat([labels_pos, labels_neg])
loss = criterion_w2v(scores, labels)
optimizer_w2v.zero_grad()
loss.backward()
optimizer_w2v.step()
losses_w2v.append(loss.item())
if (epoca + 1) % 50 == 0:
print(f" Época {epoca+1}: loss={loss.item():.4f}")
# Extrair embeddings
embeddings_w2v = {
palavra: modelo_w2v.get_embedding(idx)
for palavra, idx in p2i_w2v.items()
}
print(f"\nEmbeddings aprendidos (dim=16):")
print(f" 'gato': {embeddings_w2v['gato'].round(3)}")
print(f" 'cachorro': {embeddings_w2v['cachorro'].round(3)}")
4. Propriedades dos Embeddings
def similaridade_cosseno(v1: np.ndarray,
v2: np.ndarray) -> float:
"""Similaridade cosseno entre dois vetores."""
norm1 = np.linalg.norm(v1)
norm2 = np.linalg.norm(v2)
if norm1 == 0 or norm2 == 0:
return 0.0
return np.dot(v1, v2) / (norm1 * norm2)
def palavras_mais_similares(palavra: str,
embeddings: dict,
top_k: int = 5) -> list:
"""Encontra as k palavras mais similares."""
if palavra not in embeddings:
return []
v_alvo = embeddings[palavra]
sims = [
(p, similaridade_cosseno(v_alvo, v))
for p, v in embeddings.items()
if p != palavra
]
return sorted(sims, key=lambda x: x[1], reverse=True)[:top_k]
# Testar similaridades
print("Palavras mais similares:")
for palavra in ["gato", "oceano", "floresta"]:
if palavra in embeddings_w2v:
similares = palavras_mais_similares(
palavra, embeddings_w2v, top_k=4
)
print(f"\n '{palavra}':")
for p, sim in similares:
print(f" '{p}': {sim:.4f}")
# Visualizar embeddings com PCA
from sklearn.decomposition import PCA
palavras_plot = list(embeddings_w2v.keys())
matriz_emb = np.array([embeddings_w2v[p]
for p in palavras_plot])
pca = PCA(n_components=2, random_state=42)
coords = pca.fit_transform(matriz_emb)
# Agrupar por categoria para colorir
grupos = {
"Felinos": ["gato", "leão", "tigre", "felino"],
"Canídeos": ["cachorro", "lobo", "canídeo"],
"Natureza": ["oceano", "mar", "floresta", "praia",
"sol", "vento", "árvore", "nuvem"],
"Outros": []
}
cores_grupo = {
"Felinos": "coral",
"Canídeos": "steelblue",
"Natureza": "mediumseagreen",
"Outros": "gray"
}
fig, ax = plt.subplots(figsize=(12, 8))
for palavra, coord in zip(palavras_plot, coords):
grupo = "Outros"
for g, palavras_g in grupos.items():
if palavra in palavras_g:
grupo = g
break
cor = cores_grupo[grupo]
ax.scatter(coord[0], coord[1], c=cor,
s=100, alpha=0.7, zorder=3)
ax.annotate(palavra, (coord[0], coord[1]),
fontsize=9,
xytext=(5, 5),
textcoords="offset points")
# Legenda
from matplotlib.patches import Patch
legenda = [Patch(color=c, label=g)
for g, c in cores_grupo.items()]
ax.legend(handles=legenda, loc="upper right")
ax.set_title("Word Embeddings — Visualização PCA 2D\n"
"Palavras semelhantes aparecem próximas",
fontsize=12)
ax.set_xlabel("PC1")
ax.set_ylabel("PC2")
plt.tight_layout()
plt.show()
5. GloVe — Global Vectors
GloVe captura estatísticas globais de co-ocorrência em vez de janelas locais.
def treinar_glove_simplificado(corpus: list,
vocab: dict,
embed_dim: int = 16,
janela: int = 3,
n_epocas: int = 100) -> dict:
"""
GloVe simplificado: treina embeddings usando
co-ocorrências globais do corpus.
"""
V = len(vocab)
# Construir matriz de co-ocorrência
co_matrix = np.zeros((V, V))
for frase in corpus:
tokens = [vocab[p] for p in frase.split()
if p in vocab]
for i, centro in enumerate(tokens):
for j in range(
max(0, i - janela),
min(len(tokens), i + janela + 1)
):
if i != j:
dist = abs(i - j)
co_matrix[centro, tokens[j]] += 1.0 / dist
# Parâmetros do GloVe
W = np.random.randn(V, embed_dim) * 0.1
W_ = np.random.randn(V, embed_dim) * 0.1
b = np.zeros(V)
b_ = np.zeros(V)
lr = 0.05
x_max = 100
alpha = 0.75
def f_peso(x):
return np.where(x < x_max,
(x / x_max) ** alpha, 1.0)
losses = []
i_nz, j_nz = np.nonzero(co_matrix)
for epoca in range(n_epocas):
loss_epoca = 0.0
perm = np.random.permutation(len(i_nz))
for k in perm[:500]: # amostragem para velocidade
i_k = i_nz[k]
j_k = j_nz[k]
x = co_matrix[i_k, j_k]
peso = f_peso(x)
# Gradiente GloVe
diff = (np.dot(W[i_k], W_[j_k])
+ b[i_k] + b_[j_k]
- np.log(x + 1))
grad = peso * diff
W[i_k] -= lr * grad * W_[j_k]
W_[j_k] -= lr * grad * W[i_k]
b[i_k] -= lr * grad
b_[j_k] -= lr * grad
loss_epoca += 0.5 * peso * diff**2
losses.append(loss_epoca)
# Embeddings finais: média de W e W'
embeddings_finais = (W + W_) / 2
return {
palavra: embeddings_finais[idx]
for palavra, idx in vocab.items()
}, losses
print("Treinando GloVe simplificado...")
embeddings_glove, losses_glove = treinar_glove_simplificado(
corpus_w2v, p2i_w2v, embed_dim=16, n_epocas=100
)
print(f"GloVe treinado!")
print(f"\nSimilaridades GloVe:")
for par in [("gato", "cachorro"),
("gato", "leão"),
("gato", "oceano")]:
p1, p2 = par
if p1 in embeddings_glove and p2 in embeddings_glove:
sim = similaridade_cosseno(
embeddings_glove[p1],
embeddings_glove[p2]
)
print(f" sim('{p1}', '{p2}') = {sim:.4f}")
6. Embeddings Contextuais — A Revolução
Embeddings estáticos têm um problema: a mesma palavra tem sempre o mesmo vetor, independente do contexto.
print("\nO Problema dos Embeddings Estáticos:")
print("=" * 50)
print()
exemplos_ambiguidade = [
("O banco está na margem do rio.",
"banco = MARGEM DE RIO"),
("Fui ao banco sacar dinheiro.",
"banco = INSTITUIÇÃO FINANCEIRA"),
("O banco do jardim é de madeira.",
"banco = ASSENTO"),
]
for frase, interpretacao in exemplos_ambiguidade:
print(f" '{frase}'")
print(f" → '{interpretacao}'")
print()
print("No Word2Vec/GloVe: 'banco' tem SEMPRE o mesmo vetor.")
print("Nos modelos contextuais (BERT, GPT): 'banco' tem vetores")
print("DIFERENTES dependendo do contexto da frase!")
# Demonstração de embedding contextual com LSTM
class EmbeddingContextual(nn.Module):
"""
Simula embeddings contextuais usando LSTM bidirecional.
Cada palavra recebe um embedding diferente dependendo do contexto.
"""
def __init__(self, vocab_size: int,
embed_dim: int = 32,
hidden_dim: int = 64):
super().__init__()
self.embedding = nn.Embedding(vocab_size,
embed_dim,
padding_idx=0)
self.bilstm = nn.LSTM(
embed_dim, hidden_dim,
batch_first=True,
bidirectional=True
)
# Projeção para mesmo tamanho que embedding
self.projecao = nn.Linear(hidden_dim * 2,
embed_dim)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Retorna representações contextuais de cada token.
x: (batch, seq_len)
"""
emb = self.embedding(x)
out, _ = self.bilstm(emb)
return self.projecao(out)
# Vocabulário e modelo
vocab_ctx = {p: i+1 for i, p in enumerate(vocab_w2v)}
vocab_ctx["<PAD>"] = 0
modelo_ctx = EmbeddingContextual(
vocab_size=len(vocab_ctx) + 1,
embed_dim=32
)
# Demonstrar: "gato" em contextos diferentes
def tokenizar(frase: str, vocab: dict,
max_len: int = 10) -> torch.Tensor:
tokens = [vocab.get(p, 0) for p in frase.split()]
tokens = tokens[:max_len]
tokens += [0] * (max_len - len(tokens))
return torch.tensor(tokens).unsqueeze(0)
frases_ctx = [
"o gato mia na floresta",
"o gato caça animais selvagens",
"o cachorro e o gato brincam",
]
modelo_ctx.eval()
with torch.no_grad():
for frase in frases_ctx:
tokens = tokenizar(frase, vocab_ctx)
embs = modelo_ctx(tokens) # (1, seq, 32)
# Posição da palavra 'gato' na frase
palavras = frase.split()
if "gato" in palavras:
pos_gato = palavras.index("gato")
emb_gato = embs[0, pos_gato, :].numpy()
print(f" 'gato' em '{frase}':")
print(f" norm={np.linalg.norm(emb_gato):.3f}, "
f"primeiros 5: {emb_gato[:5].round(3)}")
7. Tokenização Moderna (BPE)
print("\n" + "=" * 60)
print("TOKENIZAÇÃO: BPE (Byte-Pair Encoding)")
print("=" * 60)
print("""
Modelos modernos como GPT e BERT não tokenizam por palavras completas.
Usam sub-palavras para lidar com palavras raras e morfologia.
Exemplos de tokenização BPE (aproximada):
"embeddings" → ["embed", "##dings"]
"tokenização" → ["token", "##iza", "##ção"]
"anticonstitucionalíssimo" → ["anti", "##con", "##stitu", ...]
Vantagens:
- Vocabulário menor (30K tokens cobre quase tudo)
- Lida com palavras fora do vocabulário
- Captura morfologia (prefixos, sufixos)
""")
# Implementação simplificada de BPE
class BPESimplificado:
"""
Implementação didática do Byte-Pair Encoding.
"""
def __init__(self, n_merges: int = 10):
self.n_merges = n_merges
self.merges = {}
self.vocab = {}
def _get_stats(self, vocab: dict) -> dict:
"""Conta pares de símbolos adjacentes."""
pairs = defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols) - 1):
pairs[(symbols[i], symbols[i+1])] += freq
return pairs
def _merge_vocab(self, pair: tuple,
v_in: dict) -> dict:
"""Funde o par mais frequente."""
v_out = {}
bigram = " ".join(pair)
replacement = "".join(pair)
for word in v_in:
w_out = word.replace(bigram, replacement)
v_out[w_out] = v_in[word]
return v_out
def treinar(self, corpus: list) -> "BPESimplificado":
"""Aprende merges do corpus."""
# Inicializar: cada palavra como sequência de chars
vocab = defaultdict(int)
for texto in corpus:
for palavra in texto.split():
# Adicionar </w> para marcar fim de palavra
vocab[" ".join(list(palavra)) + " </w>"] += 1
self.vocab = dict(vocab)
for i in range(self.n_merges):
pairs = self._get_stats(self.vocab)
if not pairs:
break
melhor = max(pairs, key=pairs.get)
self.merges[melhor] = i
self.vocab = self._merge_vocab(
melhor, self.vocab
)
return self
def tokenizar(self, palavra: str) -> list:
"""Tokeniza uma palavra usando os merges aprendidos."""
word = " ".join(list(palavra)) + " </w>"
pairs = self._get_stats({word: 1})
while True:
if not pairs:
break
# Encontrar o merge mais prioritário
melhor = min(
pairs,
key=lambda p: self.merges.get(p, float("inf"))
)
if melhor not in self.merges:
break
bigram = " ".join(melhor)
word = word.replace(bigram, "".join(melhor))
pairs = self._get_stats({word: 1})
return word.split()
# Treinar BPE
bpe = BPESimplificado(n_merges=15)
bpe.treinar(corpus_w2v)
print("BPE Simplificado — Merges aprendidos:")
for par, ordem in list(bpe.merges.items())[:5]:
print(f" {par[0]} + {par[1]} → {''.join(par)}")
print("\nTokenização de palavras:")
for palavra in ["gato", "cachorro", "felino",
"anticonstitucional", "tokenização"]:
tokens = bpe.tokenizar(palavra)
print(f" '{palavra}' → {tokens}")
8. Usando Embeddings Pré-treinados
print("\n" + "=" * 60)
print("USANDO EMBEDDINGS PRÉ-TREINADOS")
print("=" * 60)
print("""
Na prática, você raramente treina embeddings do zero.
Use embeddings pré-treinados em bilhões de palavras:
GloVe: https://nlp.stanford.edu/projects/glove/
Word2Vec: https://code.google.com/archive/p/word2vec/
FastText: https://fasttext.cc/
Para português, use:
- NILC: http://nilc.icmc.usp.br/embeddings
- FastText PT-BR: https://fasttext.cc/docs/en/crawl-vectors.html
""")
# Simulação de embeddings pré-treinados
def simular_embeddings_pretreinados(dim: int = 50) -> dict:
"""
Simula um conjunto de embeddings pré-treinados.
Em produção, você carregaria de um arquivo .txt ou .bin.
"""
np.random.seed(42)
palavras = ["gato", "cachorro", "felino", "animal",
"casa", "rua", "cidade", "país",
"azul", "vermelho", "cor",
"correr", "andar", "pular"]
# Simular clusters semânticos
embeddings = {}
for i, p in enumerate(palavras):
# Palavras do mesmo grupo ficam próximas
if p in ["gato", "cachorro", "felino", "animal"]:
base = np.array([1, 1, 0, 0, 0])
elif p in ["casa", "rua", "cidade", "país"]:
base = np.array([0, 0, 1, 1, 0])
elif p in ["azul", "vermelho", "cor"]:
base = np.array([0, 0, 0, 0, 1])
else:
base = np.array([0, 1, 0, 1, 0])
base_ext = np.tile(base, dim // 5 + 1)[:dim]
ruido = np.random.randn(dim) * 0.3
embeddings[p] = (base_ext + ruido).astype(np.float32)
return embeddings
emb_pretreinados = simular_embeddings_pretreinados(dim=50)
# Usar em PyTorch
vocab_pt = {p: i for i, p in enumerate(
emb_pretreinados.keys()
)}
matriz_emb_pt = np.array(list(emb_pretreinados.values()))
embedding_tensor = torch.from_numpy(matriz_emb_pt)
class ModeloComEmbeddingsPretreinados(nn.Module):
"""
Modelo NLP usando embeddings pré-treinados.
"""
def __init__(self,
embedding_tensor: torch.Tensor,
n_classes: int,
congelar: bool = True):
super().__init__()
vocab_size, embed_dim = embedding_tensor.shape
self.embedding = nn.Embedding(vocab_size, embed_dim)
# Carregar pesos pré-treinados
self.embedding.weight.data.copy_(embedding_tensor)
if congelar:
# Congelar: não atualizar durante treino
self.embedding.weight.requires_grad = False
print("Embeddings congelados (feature extraction)")
else:
print("Embeddings descongelados (fine-tuning)")
self.classificador = nn.Sequential(
nn.Linear(embed_dim, 32),
nn.ReLU(),
nn.Linear(32, n_classes)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# Média dos embeddings (mean pooling simples)
emb = self.embedding(x)
mean = emb.mean(dim=1)
return self.classificador(mean)
# Instanciar modelo
modelo_pretrain = ModeloComEmbeddingsPretreinados(
embedding_tensor=embedding_tensor,
n_classes=3,
congelar=True
)
n_params_total = sum(p.numel()
for p in modelo_pretrain.parameters())
n_params_train = sum(
p.numel() for p in modelo_pretrain.parameters()
if p.requires_grad
)
print(f" Total: {n_params_total:,} params")
print(f" Treináveis: {n_params_train:,} params")
print(f" Congelados: {n_params_total - n_params_train:,} params")
9. Análise de Analogias
print("\n" + "=" * 60)
print("ANALOGIAS COM WORD EMBEDDINGS")
print("=" * 60)
print("""
A propriedade mais famosa dos word embeddings:
vetor("rei") - vetor("homem") + vetor("mulher") ≈ vetor("rainha")
vetor("Paris") - vetor("França") + vetor("Alemanha") ≈ vetor("Berlim")
""")
def resolver_analogia(a: str, b: str, c: str,
embeddings: dict,
top_k: int = 3) -> list:
"""
Resolve analogia: a está para b assim como c está para ?
Fórmula: emb(b) - emb(a) + emb(c)
"""
if not all(p in embeddings for p in [a, b, c]):
return []
vetor_alvo = (embeddings[b] - embeddings[a]
+ embeddings[c])
vetor_alvo /= (np.linalg.norm(vetor_alvo) + 1e-8)
excluir = {a, b, c}
sims = []
for p, v in embeddings.items():
if p not in excluir:
v_norm = v / (np.linalg.norm(v) + 1e-8)
sims.append((p, np.dot(vetor_alvo, v_norm)))
return sorted(sims, key=lambda x: x[1],
reverse=True)[:top_k]
# Testar com nossos embeddings treinados
print("Analogias (com corpus limitado):")
analogias = [
("gato", "felino", "cachorro"),
("gato", "animal", "oceano"),
]
for a, b, c in analogias:
resultado = resolver_analogia(
a, b, c, embeddings_w2v
)
if resultado:
pred = resultado[0][0]
sim = resultado[0][1]
print(f" '{a}' → '{b}', '{c}' → '{pred}' "
f"(sim={sim:.4f})")
print(f" (Analogia: '{a}' está para '{b}' "
f"assim como '{c}' está para '{pred}')")
10. Exemplo Completo: Pipeline de NLP
print("\n" + "=" * 60)
print("PIPELINE COMPLETO DE NLP COM EMBEDDINGS")
print("=" * 60)
from torch.utils.data import Dataset, DataLoader
class TextoDataset(Dataset):
"""Dataset para classificação de texto."""
def __init__(self, textos: list,
labels: list,
vocab: dict,
max_len: int = 20):
self.textos = textos
self.labels = labels
self.vocab = vocab
self.max_len = max_len
def __len__(self):
return len(self.textos)
def __getitem__(self, idx):
tokens = [self.vocab.get(p, 1) # 1 = <UNK>
for p in self.textos[idx].split()]
# Truncar e fazer padding
tokens = tokens[:self.max_len]
tokens += [0] * (self.max_len - len(tokens))
return (torch.tensor(tokens, dtype=torch.long),
torch.tensor(self.labels[idx],
dtype=torch.float))
class ModeloNLPCompleto(nn.Module):
"""
Pipeline NLP completo:
Tokenização → Embedding → BiLSTM → Atenção → Classificação
"""
def __init__(self, vocab_size: int,
embed_dim: int = 32,
hidden_dim: int = 64,
n_classes: int = 1,
dropout: float = 0.3):
super().__init__()
self.embedding = nn.Embedding(
vocab_size, embed_dim, padding_idx=0
)
self.bilstm = nn.LSTM(
embed_dim, hidden_dim,
batch_first=True,
bidirectional=True,
num_layers=2,
dropout=dropout
)
# Atenção para ponderar passos
self.atencao = nn.Linear(hidden_dim * 2, 1)
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(hidden_dim * 2, n_classes)
def forward(self, x: torch.Tensor) -> torch.Tensor:
emb = self.dropout(self.embedding(x))
output, _ = self.bilstm(emb)
# Atenção: peso para cada posição
pesos = torch.softmax(
self.atencao(output), dim=1
)
ctx = (pesos * output).sum(dim=1)
ctx = self.dropout(ctx)
return self.fc(ctx)
# Dataset sintético de reviews
np.random.seed(42)
n_reviews = 1000
palavras_positivas = ["ótimo", "excelente", "adorei",
"incrível", "perfeito", "recomendo"]
palavras_negativas = ["péssimo", "horrível", "odiei",
"terrível", "ruim", "decepcionante"]
palavras_neutras = ["produto", "chegou", "comprei",
"é", "o", "um", "para", "de",
"com", "na", "no", "foi"]
reviews, labels_rev = [], []
for _ in range(n_reviews):
label = np.random.randint(0, 2)
if label == 1:
n_pos = np.random.randint(2, 5)
n_neg = np.random.randint(0, 2)
else:
n_pos = np.random.randint(0, 2)
n_neg = np.random.randint(2, 5)
palavras = (
list(np.random.choice(palavras_positivas, n_pos)) +
list(np.random.choice(palavras_negativas, n_neg)) +
list(np.random.choice(palavras_neutras,
np.random.randint(3, 8)))
)
np.random.shuffle(palavras)
reviews.append(" ".join(palavras))
labels_rev.append(label)
# Construir vocabulário
todas_rev = " ".join(reviews).split()
vocab_rev = {"<PAD>": 0, "<UNK>": 1}
for p in sorted(set(todas_rev)):
vocab_rev[p] = len(vocab_rev)
# Dividir dataset
n_tr_rev = int(0.7 * n_reviews)
n_va_rev = int(0.15 * n_reviews)
ds_tr_rev = TextoDataset(
reviews[:n_tr_rev], labels_rev[:n_tr_rev],
vocab_rev
)
ds_va_rev = TextoDataset(
reviews[n_tr_rev:n_tr_rev+n_va_rev],
labels_rev[n_tr_rev:n_tr_rev+n_va_rev],
vocab_rev
)
ds_te_rev = TextoDataset(
reviews[n_tr_rev+n_va_rev:],
labels_rev[n_tr_rev+n_va_rev:],
vocab_rev
)
dl_tr_rev = DataLoader(ds_tr_rev, batch_size=64,
shuffle=True)
dl_va_rev = DataLoader(ds_va_rev, batch_size=64)
dl_te_rev = DataLoader(ds_te_rev, batch_size=64)
# Treinar
device_nlp = device
modelo_nlp = ModeloNLPCompleto(
vocab_size=len(vocab_rev),
embed_dim=32, hidden_dim=64,
n_classes=1, dropout=0.3
).to(device_nlp)
n_nlp = sum(p.numel() for p in modelo_nlp.parameters())
print(f"\nModelo NLP: {n_nlp:,} parâmetros")
print(f"Vocab: {len(vocab_rev)} tokens")
opt_nlp = torch.optim.AdamW(
modelo_nlp.parameters(),
lr=1e-3, weight_decay=1e-4
)
crit_nlp = nn.BCEWithLogitsLoss()
melhor_nlp = float("inf")
hist_nlp = {"loss_tr": [], "loss_va": [],
"acc_tr": [], "acc_va": []}
print(f"\nTreinando por 20 épocas...")
for epoca in range(20):
modelo_nlp.train()
losses_tr, n_c_tr, n_t_tr = [], 0, 0
for X_b, y_b in dl_tr_rev:
X_b = X_b.to(device_nlp)
y_b = y_b.to(device_nlp)
opt_nlp.zero_grad()
logits = modelo_nlp(X_b).squeeze()
loss = crit_nlp(logits, y_b)
loss.backward()
nn.utils.clip_grad_norm_(
modelo_nlp.parameters(), 1.0
)
opt_nlp.step()
losses_tr.append(loss.item())
preds = (torch.sigmoid(logits) >= 0.5)
n_c_tr += (preds == y_b.bool()).sum().item()
n_t_tr += len(y_b)
modelo_nlp.eval()
losses_va, n_c_va, n_t_va = [], 0, 0
with torch.no_grad():
for X_b, y_b in dl_va_rev:
X_b = X_b.to(device_nlp)
y_b = y_b.to(device_nlp)
logits = modelo_nlp(X_b).squeeze()
loss = crit_nlp(logits, y_b)
losses_va.append(loss.item())
preds = (torch.sigmoid(logits) >= 0.5)
n_c_va += (preds == y_b.bool()).sum().item()
n_t_va += len(y_b)
loss_tr = np.mean(losses_tr)
acc_tr = n_c_tr / n_t_tr
loss_va = np.mean(losses_va)
acc_va = n_c_va / n_t_va
hist_nlp["loss_tr"].append(loss_tr)
hist_nlp["loss_va"].append(loss_va)
hist_nlp["acc_tr"].append(acc_tr)
hist_nlp["acc_va"].append(acc_va)
if loss_va < melhor_nlp:
melhor_nlp = loss_va
torch.save(modelo_nlp.state_dict(),
"nlp_melhor.pt")
if (epoca + 1) % 5 == 0:
print(f" Época {epoca+1:2d}/20 | "
f"Loss: {loss_tr:.4f}/{loss_va:.4f} | "
f"Acc: {acc_tr:.4f}/{acc_va:.4f}")
# Teste final
modelo_nlp.load_state_dict(
torch.load("nlp_melhor.pt", weights_only=True)
)
modelo_nlp.eval()
preds_rev, labels_rev_t = [], []
with torch.no_grad():
for X_b, y_b in dl_te_rev:
X_b = X_b.to(device_nlp)
p = (torch.sigmoid(
modelo_nlp(X_b).squeeze()
) >= 0.5).cpu().numpy()
preds_rev.extend(p.astype(int))
labels_rev_t.extend(y_b.numpy().astype(int))
acc_rev = accuracy_score(labels_rev_t, preds_rev)
print(f"\nAcurácia no teste: {acc_rev:.4f}")
# Visualização final
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ep = range(1, 21)
axes[0].plot(ep, hist_nlp["loss_tr"],
label="Treino", color="steelblue",
linewidth=2)
axes[0].plot(ep, hist_nlp["loss_va"],
label="Validação", color="coral",
linewidth=2, linestyle="--")
axes[0].set_xlabel("Época")
axes[0].set_ylabel("BCE Loss")
axes[0].set_title("Loss de Treinamento")
axes[0].legend()
axes[1].plot(ep, hist_nlp["acc_tr"],
label="Treino", color="steelblue",
linewidth=2)
axes[1].plot(ep, hist_nlp["acc_va"],
label="Validação", color="coral",
linewidth=2, linestyle="--")
axes[1].set_xlabel("Época")
axes[1].set_ylabel("Acurácia")
axes[1].set_title("Acurácia")
axes[1].legend()
plt.suptitle("Pipeline NLP Completo com Embeddings",
fontsize=13)
plt.tight_layout()
plt.show()
Resumo da Aula
- One-Hot encoding é esparso e sem semântica — inadequado para NLP moderno
- Bag of Words conta frequências mas ignora ordem das palavras
- TF-IDF pondera palavras pelo quão informativas são: frequente no doc, rara no corpus
- Word2Vec aprende embeddings densos a partir de contextos locais (janela deslizante)
- GloVe aprende de co-ocorrências globais — melhor para capturar relações semânticas
- Embeddings estáticos têm o mesmo vetor para cada palavra independente do contexto
- Embeddings contextuais (BERT, GPT) geram vetores diferentes para a mesma palavra em contextos diferentes
- BPE (Byte-Pair Encoding) tokeniza em sub-palavras — lida com palavras raras e morfologia
- Embeddings pré-treinados podem ser congelados (feature extraction) ou ajustados (fine-tuning)
- A propriedade de analogia (king - man + woman ≈ queen) emerge naturalmente dos embeddings treinados
- Mean pooling sobre embeddings é um baseline simples mas eficaz para classificação de texto
Exercícios
-
Explique a diferença entre TF e IDF no TF-IDF. Por que uma palavra como "o" tem IDF baixo? E uma palavra como "anticonstitucional"? Como o TF-IDF equilibra esses dois fatores?
✓ Resposta:TF (Term Frequency) mede com que frequência um termo aparece em um documento específico. Uma palavra que aparece 10 vezes em um documento tem TF maior que uma que aparece 2 vezes no mesmo documento.
IDF (Inverse Document Frequency) mede o quão rara é a palavra no corpus inteiro:
IDF = log(N / df). Uma palavra que aparece em todos os documentos temdf = N, entãoIDF = log(1) = 0— é completamente penalizada. Uma palavra que aparece em apenas 1 documento tem IDF alto."O" tem IDF próximo de zero porque aparece em praticamente todos os documentos — é uma stop word sem poder discriminativo. "Anticonstitucional" teria IDF muito alto porque aparece em pouquíssimos documentos — quando aparece, é muito informativa sobre o conteúdo.
O TF-IDF multiplica os dois:
TF-IDF = TF × IDF. Uma palavra comum em um documento específico mas rara no corpus geral recebe score alto — é relevante para aquele documento mas não para todos. Uma palavra comum em todos os documentos recebe score próximo de zero independente da frequência local. -
Explique a hipótese distribucional que fundamenta o Word2Vec. Por que palavras que aparecem em contextos similares tendem a ter significados similares? Dê um exemplo concreto.
✓ Resposta:A hipótese distribucional, formulada pelo linguista Zellig Harris (1954): "você conhece uma palavra pela companhia que ela mantém". Palavras que aparecem nos mesmos contextos tendem a ter significados relacionados.
Exemplo concreto: considere as frases: - "O ___ late." - "O ___ corre no parque." - "Dei comida para o ___." - "O ___ é o melhor amigo do homem."
As palavras "cachorro", "cão" e "vira-lata" poderiam preencher todos esses contextos. Word2Vec aprende que essas palavras têm vetores similares porque aparecem em ambientes linguísticos similares, mesmo sem nunca saber explicitamente que são sinônimos ou hipônimos.
O mecanismo no Skip-Gram: ao tentar prever "late" e "parque" dado "cachorro" como input, e também prever "late" e "parque" dado "cão", os pesos de "cachorro" e "cão" são ajustados na mesma direção. Após milhões de exemplos, eles convergem para vetores próximos.
-
Qual é a diferença entre Skip-Gram e CBOW no Word2Vec? Em qual situação cada um tende a ser melhor? Por que usar negative sampling no treinamento?
✓ Resposta:Skip-Gram: dada uma palavra central, prevê as palavras de contexto ao redor. Funciona melhor para vocabulários grandes e palavras raras, porque cada palavra tem múltiplos exemplos de treinamento (uma para cada palavra de contexto). É o método mais popular.
CBOW (Continuous Bag of Words): dado o contexto (palavras ao redor), prevê a palavra central. Mais rápido de treinar e funciona melhor para palavras frequentes, pois o contexto fornece mais informação.
Negative sampling é necessário porque sem ele, o treinamento exigiria calcular a probabilidade softmax sobre todo o vocabulário (ex: 50.000 palavras) a cada atualização — computacionalmente proibitivo. Com negative sampling, ao treinar um par positivo (centro, contexto), você também treina com k pares negativos (centro, palavra_aleatória). Isso transforma um problema de softmax multi-classe em k+1 classificações binárias independentes, reduzindo a complexidade de O(V) para O(k) por passo.
-
O que são embeddings contextuais e por que eles são superiores aos embeddings estáticos para a maioria das tarefas de NLP? Use o exemplo da palavra "banco" para ilustrar a diferença.
✓ Resposta:Embeddings estáticos (Word2Vec, GloVe) atribuem um único vetor fixo para cada palavra, independente do contexto. O vetor de "banco" é sempre o mesmo, seja em "banco de dados", "banco do Brasil" ou "banco de jardim".
Embeddings contextuais geram um vetor diferente para a mesma palavra dependendo do contexto completo da frase. Ao processar "fui ao banco sacar dinheiro", o modelo BERT atribui ao token "banco" um vetor próximo de "agência", "financeiro", "dinheiro". Ao processar "sentei no banco da praça", o mesmo token recebe um vetor próximo de "assento", "madeira", "jardim".
Para tarefas como desambiguação de sentido, Named Entity Recognition e perguntas e respostas, essa distinção é fundamental. Um classificador de sentimentos que usa embedding estático de "banco" pode confundir críticas financeiras com reclamações de mobiliário. Embeddings contextuais capturam o significado real em cada ocorrência.
Modelos contextuais são superiores porque aprendem representações em camadas profundas que capturam sintaxe (camadas baixas), semântica (camadas médias) e pragmática (camadas altas) de forma integrada com o contexto.
-
Explique o Byte-Pair Encoding (BPE). Por que tokenizar em sub-palavras é melhor que tokenizar em palavras inteiras para modelos de linguagem modernos?
✓ Resposta:BPE começa com caracteres individuais e iterativamente funde os pares de símbolos mais frequentes no corpus. Após os merges, palavras comuns como "the" e "is" tornam-se tokens únicos, enquanto palavras raras são divididas em sub-unidades reconhecíveis.
Vantagens sobre tokenização por palavras:
Cobertura de vocabulário: com 30.000 sub-palavras, o BPE cobre praticamente qualquer texto em inglês/português, enquanto um vocabulário de 30.000 palavras inteiras deixaria palavras raras como
<UNK>. Nunca há tokens desconhecidos puros.Morfologia: BPE captura naturalmente prefixos e sufixos. "tokenização", "tokenizar" e "tokenizador" provavelmente compartilham o sub-token "token", capturando a relação morfológica.
Palavras novas: novas palavras técnicas, jargões e erros de ortografia são decompostos em sub-tokens conhecidos em vez de serem descartados.
Multilinguismo: BPE pode ser aplicado a qualquer idioma sem conhecimento linguístico prévio, tornando os modelos mais facilmente multilíngues.
-
Quando você usaria embeddings congelados vs fine-tuning em um projeto de NLP? Quais fatores determinam essa escolha e quais são os riscos de cada abordagem?
✓ Resposta:Embeddings congelados são adequados quando: o dataset de fine-tuning é muito pequeno (risco alto de overfitting nos embeddings), o domínio do fine-tuning é similar ao pré-treinamento, você quer treino mais rápido e menor custo computacional, ou os embeddings pré-treinados já capturam bem o vocabulário relevante.
Fine-tuning dos embeddings é adequado quando: o dataset é suficientemente grande para atualizar os embeddings sem overfitting (regra prática: pelo menos algumas centenas de exemplos por classe), o domínio é muito específico (medicina, direito, código de programação) e os embeddings gerais não capturam bem o vocabulário especializado, ou a performance máxima é necessária e os recursos computacionais permitem.
Riscos do congelamento: o modelo não pode adaptar as representações ao domínio específico — "bug" no contexto de software tem significado diferente de "bug" no contexto de entomologia, e embeddings congelados de texto geral podem não capturar essa distinção.
Riscos do fine-tuning: catastrofic forgetting — ao atualizar os embeddings no dataset pequeno, o modelo pode perder o conhecimento geral adquirido no pré-treinamento. Usar learning rate muito baixo (1e-5 a 1e-4) para os embeddings mitiga esse risco.