Objetivo
Entender a arquitetura interna de GPT e BERT, como o pré-treinamento os torna tão poderosos, as diferenças fundamentais entre modelos encoder-only e decoder-only, e como usar esses modelos via Hugging Face para tarefas práticas sem treinar do zero.
1. O que são LLMs e por que são revolucionários?
Large Language Models (LLMs) são modelos Transformer treinados em quantidades massivas de texto com o objetivo de aprender a representação estatística da linguagem. O que os torna revolucionários não é a arquitetura em si (é o Transformer da aula anterior), mas a escala e a forma de treinamento:
- GPT-3: 175 bilhões de parâmetros, treinado em ~300 bilhões de tokens
- BERT-large: 340 milhões de parâmetros, treinado em toda a Wikipedia + BookCorpus
- LLaMA-3: 70 bilhões de parâmetros, treinado em 15 trilhões de tokens
A propriedade mais surpreendente: capacidades emergentes — comportamentos complexos que surgem espontaneamente em modelos grandes sem serem explicitamente programados, como raciocínio em poucos passos, tradução zero-shot e geração de código.
2. BERT — Bidirectional Encoder Representations from Transformers
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import math
import warnings
warnings.filterwarnings("ignore")
torch.manual_seed(42)
np.random.seed(42)
sns.set_theme(style="whitegrid")
device = torch.device("cuda" if torch.cuda.is_available()
else "cpu")
print(f"Dispositivo: {device}")
# ── Arquitetura BERT ──────────────────────────────────────
print("BERT — Arquitetura e Pré-treinamento:")
print("=" * 55)
print("""
Arquitetura: Transformer Encoder (bidirecional)
BERT-base: 12 camadas, 768 dimensões, 12 cabeças → 110M params
BERT-large: 24 camadas, 1024 dimensões, 16 cabeças → 340M params
Tokens especiais:
[CLS] → começo de toda sequência (usado para classificação)
[SEP] → separador entre sentenças (pares de texto)
[MASK] → token mascarado para MLM
[PAD] → preenchimento para lotes
Formato de entrada:
[CLS] token₁ token₂ ... [SEP] token₁ token₂ ... [SEP]
↑ ↑
Sentença A Sentença B
Pré-treinamento com 2 tarefas simultâneas:
1. MLM (Masked Language Model): prever tokens mascarados
2. NSP (Next Sentence Prediction): prever se B segue A
""")
class BERTMiniatura(nn.Module):
"""
BERT em miniatura para fins didáticos.
Implementa MLM e NSP como no paper original.
"""
def __init__(self,
vocab_size: int = 1000,
d_model: int = 128,
n_heads: int = 4,
n_camadas: int = 4,
d_ff: int = 512,
max_len: int = 128,
n_segmentos: int = 2,
dropout: float = 0.1):
super().__init__()
self.d_model = d_model
# Três tipos de embedding somados
self.token_embedding = nn.Embedding(
vocab_size, d_model, padding_idx=0
)
self.posicao_embedding = nn.Embedding(
max_len, d_model
)
self.segmento_embedding = nn.Embedding(
n_segmentos + 1, d_model
)
self.norm_emb = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
# Camadas Transformer Encoder
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model,
nhead=n_heads,
dim_feedforward=d_ff,
dropout=dropout,
batch_first=True,
norm_first=True # Pre-LN
)
self.encoder = nn.TransformerEncoder(
encoder_layer, num_layers=n_camadas,
norm=nn.LayerNorm(d_model)
)
# Cabeça de MLM (Masked Language Modeling)
self.mlm_head = nn.Sequential(
nn.Linear(d_model, d_model),
nn.GELU(),
nn.LayerNorm(d_model),
nn.Linear(d_model, vocab_size)
)
# Cabeça de NSP (Next Sentence Prediction)
self.pooler = nn.Sequential(
nn.Linear(d_model, d_model),
nn.Tanh()
)
self.nsp_head = nn.Linear(d_model, 2)
self._inicializar()
def _inicializar(self):
for p in self.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
def forward(self,
tokens: torch.Tensor,
segmentos: torch.Tensor = None,
mask_padding: torch.Tensor = None) -> dict:
"""
tokens: (batch, seq_len) — índices
segmentos: (batch, seq_len) — 0 para A, 1 para B
mask_padding: (batch, seq_len) — True onde é padding
"""
batch, seq_len = tokens.shape
# Posições
posicoes = torch.arange(seq_len, device=tokens.device)
posicoes = posicoes.unsqueeze(0).expand(batch, -1)
# Segmentos (padrão: tudo segmento 0)
if segmentos is None:
segmentos = torch.zeros_like(tokens)
# Embedding total = token + posição + segmento
x = (self.token_embedding(tokens)
+ self.posicao_embedding(posicoes)
+ self.segmento_embedding(segmentos))
x = self.dropout(self.norm_emb(x))
# Máscara de padding para o Transformer
if mask_padding is not None:
src_key_padding_mask = mask_padding
else:
src_key_padding_mask = (tokens == 0)
# Encoder
hidden = self.encoder(
x,
src_key_padding_mask=src_key_padding_mask
)
# Saídas para as duas tarefas
# MLM: predição para todos os tokens
logits_mlm = self.mlm_head(hidden)
# NSP: usar representação do [CLS] (posição 0)
cls_rep = self.pooler(hidden[:, 0, :])
logits_nsp = self.nsp_head(cls_rep)
return {
"hidden": hidden,
"logits_mlm": logits_mlm,
"logits_nsp": logits_nsp,
"cls_rep": cls_rep,
}
# Instanciar BERT miniatura
bert = BERTMiniatura(
vocab_size=1000,
d_model=128,
n_heads=4,
n_camadas=4,
d_ff=256,
max_len=64
).to(device)
n_params_bert = sum(p.numel() for p in bert.parameters())
print(f"\nBERT Miniatura:")
print(f" Parâmetros: {n_params_bert:,}")
print(f" (BERT-base real: ~110 milhões)")
# Teste forward
tokens_demo = torch.randint(1, 1000, (2, 20)).to(device)
seg_demo = torch.zeros_like(tokens_demo)
seg_demo[:, 10:] = 1 # segunda sentença
saidas_bert = bert(tokens_demo, seg_demo)
print(f"\nForward pass BERT:")
print(f" Input: {tokens_demo.shape}")
print(f" Hidden: {saidas_bert['hidden'].shape}")
print(f" MLM logits: {saidas_bert['logits_mlm'].shape}")
print(f" NSP logits: {saidas_bert['logits_nsp'].shape}")
print(f" CLS rep: {saidas_bert['cls_rep'].shape}")
3. Pré-treinamento BERT: MLM e NSP
def criar_dados_mlm(tokens: torch.Tensor,
vocab_size: int,
mask_prob: float = 0.15,
mask_token_id: int = 103) -> tuple:
"""
Implementa o mascaramento do BERT para MLM.
Estratégia:
80% → substitui por [MASK]
10% → substitui por token aleatório
10% → mantém o token original
"""
labels = tokens.clone()
mascara = torch.rand(tokens.shape) < mask_prob
# Não mascarar tokens especiais (CLS, SEP, PAD)
mascara &= (tokens > 3)
# 80% → [MASK]
proporcao_mask = torch.rand(tokens.shape) < 0.8
tokens_mod = tokens.clone()
tokens_mod[mascara & proporcao_mask] = mask_token_id
# 10% → token aleatório (dos 80% restantes, metade)
proporcao_random = (~proporcao_mask) & (torch.rand(tokens.shape) < 0.5)
tokens_random = torch.randint(4, vocab_size, tokens.shape)
tokens_mod[mascara & proporcao_random] = tokens_random[mascara & proporcao_random]
# Onde não foi mascarado, labels = -100 (ignorado pelo CrossEntropy)
labels[~mascara] = -100
return tokens_mod, labels, mascara
# Demonstrar MLM
print("\nDemonstração do Mascaramento MLM:")
tokens_ex = torch.tensor([[5, 23, 67, 12, 89, 34, 56, 4]])
tokens_mod, labels_mlm, mascara = criar_dados_mlm(
tokens_ex, vocab_size=1000
)
print(f" Original: {tokens_ex[0].tolist()}")
print(f" Mascarado: {tokens_mod[0].tolist()}")
print(f" Labels: {labels_mlm[0].tolist()}")
print(f" Máscara: {mascara[0].tolist()}")
print(f" (Labels -100 = não mascarado, ignorado na loss)")
# Loop de pré-treinamento simplificado
def pretreinamento_bert_epoch(modelo: BERTMiniatura,
dataloader,
optimizer,
device: torch.device,
vocab_size: int) -> dict:
"""Executa uma época de pré-treinamento BERT."""
modelo.train()
crit_mlm = nn.CrossEntropyLoss(ignore_index=-100)
crit_nsp = nn.CrossEntropyLoss()
losses_mlm, losses_nsp = [], []
for batch in dataloader:
tokens, segmentos, labels_nsp = batch
tokens = tokens.to(device)
segmentos = segmentos.to(device)
labels_nsp = labels_nsp.to(device)
# Mascaramento para MLM
tokens_masked, labels_mlm, _ = criar_dados_mlm(
tokens, vocab_size
)
labels_mlm = labels_mlm.to(device)
optimizer.zero_grad()
saidas = modelo(tokens_masked, segmentos)
# Loss MLM
loss_mlm = crit_mlm(
saidas["logits_mlm"].view(-1, vocab_size),
labels_mlm.view(-1)
)
# Loss NSP
loss_nsp = crit_nsp(saidas["logits_nsp"], labels_nsp)
# Loss total
loss = loss_mlm + loss_nsp
loss.backward()
nn.utils.clip_grad_norm_(modelo.parameters(), 1.0)
optimizer.step()
losses_mlm.append(loss_mlm.item())
losses_nsp.append(loss_nsp.item())
return {
"loss_mlm": np.mean(losses_mlm),
"loss_nsp": np.mean(losses_nsp),
}
print("\nPré-treinamento BERT (conceitual — dados sintéticos):")
print(" Tarefa 1 — MLM: prever tokens [MASK]")
print(" Tarefa 2 — NSP: prever se sentença B segue sentença A")
print(" Na prática: treina em Wikipedia + BookCorpus por dias/semanas em GPUs")
4. GPT — Generative Pre-trained Transformer
print("\n" + "=" * 55)
print("GPT — Arquitetura e Geração Autoregressiva")
print("=" * 55)
print("""
Arquitetura: Transformer Decoder (apenas)
GPT-1: 12 camadas, 768 dim, 12 cabeças → 117M params
GPT-2: 48 camadas, 1600 dim, 25 cabeças → 1.5B params
GPT-3: 96 camadas, 12288 dim, 96 cabeças → 175B params
GPT-4: estimado >1T params (arquitetura não revelada)
Diferença fundamental do BERT:
- Unidirecional (causal): cada token só vê tokens anteriores
- Treinado para prever o PRÓXIMO token
- Não usa [MASK] — aprende de todo o texto disponível
Pré-treinamento: Language Modeling
Dado: "O gato sentou no ___"
Prever: "tapete"
Geração autoregressiva:
"O" → prever "gato"
"O gato" → prever "sentou"
"O gato sentou" → prever "no"
...
""")
class GPTMiniatura(nn.Module):
"""
GPT em miniatura: Transformer Decoder-only.
"""
def __init__(self,
vocab_size: int = 1000,
d_model: int = 128,
n_heads: int = 4,
n_camadas: int = 4,
d_ff: int = 512,
max_len: int = 256,
dropout: float = 0.1):
super().__init__()
self.d_model = d_model
self.max_len = max_len
self.vocab_size = vocab_size
self.embedding = nn.Embedding(vocab_size, d_model,
padding_idx=0)
self.pos_emb = nn.Embedding(max_len, d_model)
self.drop = nn.Dropout(dropout)
# Decoder-only: cada camada tem APENAS masked self-attention
decoder_layer = nn.TransformerDecoderLayer(
d_model=d_model,
nhead=n_heads,
dim_feedforward=d_ff,
dropout=dropout,
batch_first=True,
norm_first=True
)
self.decoder = nn.TransformerDecoder(
decoder_layer, num_layers=n_camadas,
norm=nn.LayerNorm(d_model)
)
# Cabeça de linguagem: projeta para vocab
self.lm_head = nn.Linear(d_model, vocab_size,
bias=False)
# Weight tying: compartilhar pesos de embedding e LM head
self.lm_head.weight = self.embedding.weight
self._inicializar()
def _inicializar(self):
for p in self.parameters():
if p.dim() > 1:
nn.init.normal_(p, std=0.02)
def _criar_mascara_causal(self,
seq_len: int) -> torch.Tensor:
"""Cria máscara triangular inferior (causal)."""
mask = torch.triu(
torch.ones(seq_len, seq_len), diagonal=1
).bool()
return mask
def forward(self,
x: torch.Tensor) -> torch.Tensor:
"""
x: (batch, seq_len)
Retorna logits: (batch, seq_len, vocab_size)
"""
batch, seq_len = x.shape
# Posições
pos = torch.arange(seq_len, device=x.device)
pos = pos.unsqueeze(0).expand(batch, -1)
# Embedding
emb = self.drop(
self.embedding(x) * math.sqrt(self.d_model)
+ self.pos_emb(pos)
)
# Máscara causal
causal_mask = self._criar_mascara_causal(
seq_len
).to(x.device)
# Em PyTorch TransformerDecoder sem encoder:
# usamos o tensor como "memory" também (hack)
out = self.decoder(
emb, emb,
tgt_mask=causal_mask,
memory_mask=causal_mask
)
# Logits sobre o vocabulário
return self.lm_head(out)
@torch.no_grad()
def gerar(self,
prompt: torch.Tensor,
max_novos_tokens: int = 50,
temperatura: float = 1.0,
top_k: int = 50,
top_p: float = 0.9) -> torch.Tensor:
"""
Gera texto autoregressivamente.
temperatura: > 1 = mais criativo, < 1 = mais conservador
top_k: manter apenas os k tokens mais prováveis
top_p: nucleus sampling — manter tokens até acumular p
"""
self.eval()
sequencia = prompt.clone()
for _ in range(max_novos_tokens):
# Truncar se exceder max_len
seq_in = sequencia[:, -self.max_len:]
# Forward
logits = self(seq_in)
# Pegar logits do ÚLTIMO token
logits = logits[:, -1, :] / temperatura
# Top-k filtering
if top_k > 0:
valores, _ = torch.topk(logits, top_k)
limiar_k = valores[:, -1].unsqueeze(-1)
logits = logits.masked_fill(
logits < limiar_k, float("-inf")
)
# Top-p (nucleus) filtering
if top_p < 1.0:
probs_sorted, indices_sorted = torch.sort(
F.softmax(logits, dim=-1),
descending=True
)
cumsum = torch.cumsum(probs_sorted, dim=-1)
remover = (cumsum - probs_sorted) > top_p
probs_sorted[remover] = 0.0
probs_sorted = probs_sorted / probs_sorted.sum()
idx_sample = torch.multinomial(
probs_sorted, 1
)
proximo = indices_sorted.gather(-1, idx_sample)
else:
probs = F.softmax(logits, dim=-1)
proximo = torch.multinomial(probs, 1)
sequencia = torch.cat([sequencia, proximo], dim=1)
return sequencia
# Instanciar GPT miniatura
gpt = GPTMiniatura(
vocab_size=1000,
d_model=128,
n_heads=4,
n_camadas=4,
d_ff=256,
max_len=128
).to(device)
n_params_gpt = sum(p.numel() for p in gpt.parameters())
print(f"\nGPT Miniatura:")
print(f" Parâmetros: {n_params_gpt:,}")
# Teste de geração
prompt_ex = torch.randint(1, 100, (1, 5)).to(device)
gerado_ex = gpt.gerar(prompt_ex, max_novos_tokens=10,
temperatura=1.0, top_k=20)
print(f"\nGeração autoregressiva:")
print(f" Prompt: {prompt_ex[0].tolist()}")
print(f" Gerado: {gerado_ex[0].tolist()}")
print(f" (índices de tokens — sem vocabulário real aqui)")
5. Pré-treinamento GPT: Language Modeling
# Dataset sintético de sequências de tokens
class LMDataset(torch.utils.data.Dataset):
"""
Dataset para Language Modeling.
Cada exemplo é uma sequência; label é a mesma sequência
deslocada em 1 posição (próximo token).
"""
def __init__(self, n: int = 5000,
seq_len: int = 32,
vocab_size: int = 200):
np.random.seed(42)
self.seqs = []
# Simular padrões: sequências com estrutura
padroes = [
list(range(1, 20)), # sequência crescente
list(range(20, 1, -1)), # sequência decrescente
[i % 10 + 1 for i in range(20)], # repetição cíclica
]
for _ in range(n):
padrao = padroes[np.random.randint(len(padroes))]
inicio = np.random.randint(0, len(padrao))
seq = []
for i in range(seq_len + 1):
idx = (inicio + i) % len(padrao)
seq.append(padrao[idx])
# Adicionar ruído
seq = [max(1, min(vocab_size-1,
s + np.random.randint(-1, 2)))
for s in seq]
self.seqs.append(seq)
def __len__(self):
return len(self.seqs)
def __getitem__(self, idx):
seq = torch.tensor(self.seqs[idx], dtype=torch.long)
return seq[:-1], seq[1:] # input, target
ds_lm = LMDataset(n=3000, seq_len=32, vocab_size=200)
dl_lm = torch.utils.data.DataLoader(
ds_lm, batch_size=64, shuffle=True
)
# Pré-treinar GPT
gpt_lm = GPTMiniatura(
vocab_size=201, d_model=64, n_heads=4,
n_camadas=3, d_ff=256, max_len=64
).to(device)
opt_lm = torch.optim.AdamW(
gpt_lm.parameters(), lr=3e-4, weight_decay=0.1
)
crit_lm = nn.CrossEntropyLoss()
print("Pré-treinando GPT por Language Modeling...")
hist_lm = []
for epoca in range(20):
gpt_lm.train()
losses_ep = []
for x_b, y_b in dl_lm:
x_b = x_b.to(device)
y_b = y_b.to(device)
opt_lm.zero_grad()
logits = gpt_lm(x_b)
loss = crit_lm(
logits.view(-1, 201),
y_b.view(-1)
)
loss.backward()
nn.utils.clip_grad_norm_(gpt_lm.parameters(), 1.0)
opt_lm.step()
losses_ep.append(loss.item())
loss_ep = np.mean(losses_ep)
hist_lm.append(loss_ep)
if (epoca + 1) % 5 == 0:
# Calcular perplexidade
perplexidade = math.exp(loss_ep)
print(f" Época {epoca+1:2d}/20 | "
f"Loss: {loss_ep:.4f} | "
f"Perplexidade: {perplexidade:.2f}")
# Testar geração após pré-treinamento
gpt_lm.eval()
prompt_test = torch.tensor([[1, 2, 3]]).to(device)
gerado_test = gpt_lm.gerar(
prompt_test,
max_novos_tokens=15,
temperatura=0.7,
top_k=10
)
print(f"\nGeração pós-treinamento:")
print(f" Prompt: {prompt_test[0].tolist()}")
print(f" Gerado: {gerado_test[0].tolist()}")
print(f" (modelo aprende padrões sequenciais)")
6. Comparação BERT vs GPT
print("\n" + "=" * 60)
print("BERT vs GPT: COMPARAÇÃO DETALHADA")
print("=" * 60)
fig, axes = plt.subplots(1, 2, figsize=(16, 8))
# Diagrama BERT
axes[0].set_xlim(0, 10)
axes[0].set_ylim(0, 10)
axes[0].axis("off")
axes[0].set_title("BERT (Encoder-only)\nBidirecional",
fontsize=13, fontweight="bold")
# Tokens
tokens_bert = ["[CLS]", "o", "gato", "[MASK]", "no", "[SEP]"]
for i, tok in enumerate(tokens_bert):
cor = "#FFB347" if tok in ["[CLS]", "[SEP]"] else \
"#FF6B6B" if tok == "[MASK]" else "#87CEEB"
rect = plt.Rectangle((i * 1.5 + 0.2, 1), 1.2, 0.6,
color=cor, ec="gray", lw=1.5)
axes[0].add_patch(rect)
axes[0].text(i * 1.5 + 0.8, 1.35, tok,
ha="center", va="center", fontsize=8,
fontweight="bold")
# Setas bidirecionais entre tokens
for i in range(len(tokens_bert)):
for j in range(len(tokens_bert)):
if i != j:
axes[0].annotate("",
xy=(j * 1.5 + 0.8, 2.5),
xytext=(i * 1.5 + 0.8, 2.5),
arrowprops=dict(
arrowstyle="->", color="steelblue",
alpha=0.15, lw=0.8
)
)
# Representações contextuais
for i, tok in enumerate(tokens_bert):
cor = "#FFB347" if tok in ["[CLS]", "[SEP]"] else \
"#FF6B6B" if tok == "[MASK]" else "#90EE90"
rect2 = plt.Rectangle((i * 1.5 + 0.2, 3.5), 1.2, 0.6,
color=cor, ec="gray", lw=1.5)
axes[0].add_patch(rect2)
axes[0].text(4.5, 4.2, "Prediz token mascarado", ha="center",
fontsize=9, color="red", fontweight="bold")
axes[0].text(4.5, 4.8, "[CLS] → classificação",
ha="center", fontsize=9, color="darkorange",
fontweight="bold")
axes[0].text(4.5, 2.0, "← → atenção bidirecional ← →",
ha="center", fontsize=9, color="steelblue")
axes[0].text(4.5, 0.5,
"Cada token vê TODOS os outros tokens",
ha="center", fontsize=10, color="gray")
# Diagrama GPT
axes[1].set_xlim(0, 10)
axes[1].set_ylim(0, 10)
axes[1].axis("off")
axes[1].set_title("GPT (Decoder-only)\nCausal (esquerda→direita)",
fontsize=13, fontweight="bold")
tokens_gpt = ["o", "gato", "sentou", "no", "tapete"]
for i, tok in enumerate(tokens_gpt):
cor = "#87CEEB"
rect = plt.Rectangle((i * 1.8 + 0.1, 1), 1.5, 0.6,
color=cor, ec="gray", lw=1.5)
axes[1].add_patch(rect)
axes[1].text(i * 1.8 + 0.85, 1.35, tok,
ha="center", va="center", fontsize=9)
# Setas causais (apenas para frente)
for i in range(len(tokens_gpt)):
for j in range(i + 1, len(tokens_gpt)):
axes[1].annotate("",
xy=(j * 1.8 + 0.85, 2.4),
xytext=(i * 1.8 + 0.85, 2.4),
arrowprops=dict(
arrowstyle="->", color="coral",
alpha=0.4, lw=1.0
)
)
axes[1].text(4.5, 2.0, "→ atenção apenas para passado →",
ha="center", fontsize=9, color="coral")
# Predições
for i, tok in enumerate(tokens_gpt):
if i < len(tokens_gpt) - 1:
axes[1].text(i * 1.8 + 0.85, 3.8,
f"→ '{tokens_gpt[i+1]}'",
ha="center", fontsize=8,
color="darkgreen")
axes[1].text(4.5, 4.8,
"Prediz PRÓXIMO token a cada passo",
ha="center", fontsize=10,
color="darkgreen", fontweight="bold")
axes[1].text(4.5, 0.5,
"Cada token vê apenas tokens ANTERIORES",
ha="center", fontsize=10, color="gray")
plt.suptitle("Diferença Fundamental: BERT vs GPT",
fontsize=14, y=0.98)
plt.tight_layout()
plt.show()
# Tabela comparativa
print("\nComparação estruturada:")
print(f"{'Aspecto':<28} {'BERT':>20} {'GPT':>20}")
print("-" * 70)
comparacoes = [
("Arquitetura", "Encoder-only", "Decoder-only"),
("Atenção", "Bidirecional", "Causal (unidirec.)"),
("Pré-treinamento", "MLM + NSP", "Language Modeling"),
("Tokens especiais", "[CLS] [SEP] [MASK]", "Nenhum especial"),
("Força principal", "Compreensão", "Geração"),
("Classificação", "Excelente", "Boa (com prompt)"),
("Geração de texto", "Não adequado", "Excelente"),
("Zero-shot", "Limitado", "Muito bom"),
("Few-shot", "Requer fine-tuning", "In-context learning"),
("Exemplo base", "BERT, RoBERTa", "GPT-2, GPT-3, LLaMA"),
]
for asp, bert_v, gpt_v in comparacoes:
print(f"{asp:<28} {bert_v:>20} {gpt_v:>20}")
7. Fine-tuning de LLMs
print("\n" + "=" * 60)
print("FINE-TUNING DE LLMs")
print("=" * 60)
print("""
Fine-tuning adapta um LLM pré-treinado para uma tarefa específica.
Estratégias:
┌────────────────────────────────────────────────────────────┐
│ Full Fine-tuning │
│ Treina TODOS os parâmetros no dataset da tarefa │
│ ✓ Melhor performance ✗ Muito caro (≈ pré-treino) │
├────────────────────────────────────────────────────────────┤
│ Feature Extraction (Linear Probing) │
│ Congela o LLM, treina apenas uma cabeça de classificação │
│ ✓ Rápido ✗ Performance limitada │
├────────────────────────────────────────────────────────────┤
│ LoRA (Low-Rank Adaptation) │
│ Adiciona matrizes de baixo rank treináveis │
│ ✓ Eficiente ✓ Boa performance ✓ PADRÃO atual │
├────────────────────────────────────────────────────────────┤
│ Prompt Tuning / Prefix Tuning │
│ Aprende soft prompts sem modificar os pesos do modelo │
│ ✓ Muito eficiente ✗ Performance variável │
└────────────────────────────────────────────────────────────┘
""")
# Simulação de fine-tuning com Linear Probing
class BERTParaClassificacao(nn.Module):
"""
Fine-tuning do BERT para classificação.
Usa a representação [CLS] + cabeça linear.
"""
def __init__(self,
bert_modelo: BERTMiniatura,
n_classes: int,
congelar_bert: bool = True,
dropout: float = 0.1):
super().__init__()
self.bert = bert_modelo
self.dropout = nn.Dropout(dropout)
self.cabeca = nn.Linear(
bert_modelo.d_model, n_classes
)
if congelar_bert:
for param in self.bert.parameters():
param.requires_grad = False
print("BERT congelado (Linear Probing)")
else:
print("BERT descongelado (Full Fine-tuning)")
n_total = sum(p.numel() for p in self.parameters())
n_treino = sum(p.numel() for p in self.parameters()
if p.requires_grad)
print(f" Parâmetros: {n_total:,} total, "
f"{n_treino:,} treináveis ({n_treino/n_total:.1%})")
def forward(self, tokens, segmentos=None):
saidas = self.bert(tokens, segmentos)
cls_rep = self.dropout(saidas["cls_rep"])
return self.cabeca(cls_rep)
# Criar datasets de classificação
class ClassificacaoDataset(torch.utils.data.Dataset):
def __init__(self, n=2000, seq_len=20, vocab_size=1000,
n_classes=3):
np.random.seed(42)
self.seqs = []
self.labels = []
for _ in range(n):
label = np.random.randint(0, n_classes)
# Padrão por classe
base = label * (vocab_size // n_classes) + 5
fim = base + vocab_size // n_classes - 5
seq = [1] # [CLS]
for _ in range(seq_len - 2):
if np.random.random() < 0.6:
seq.append(int(np.random.randint(base, fim)))
else:
seq.append(int(np.random.randint(5, 100)))
seq.append(2) # [SEP]
self.seqs.append(seq)
self.labels.append(label)
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
return (
torch.tensor(self.seqs[idx], dtype=torch.long),
torch.tensor(self.labels[idx], dtype=torch.long)
)
ds_clf = ClassificacaoDataset(n=2000, seq_len=20,
vocab_size=1000, n_classes=3)
n_tr_c = int(0.7 * len(ds_clf))
n_va_c = int(0.15 * len(ds_clf))
n_te_c = len(ds_clf) - n_tr_c - n_va_c
from torch.utils.data import random_split
ds_tr_c, ds_va_c, ds_te_c = random_split(
ds_clf, [n_tr_c, n_va_c, n_te_c],
generator=torch.Generator().manual_seed(42)
)
dl_tr_c = torch.utils.data.DataLoader(
ds_tr_c, batch_size=64, shuffle=True
)
dl_va_c = torch.utils.data.DataLoader(ds_va_c, batch_size=64)
dl_te_c = torch.utils.data.DataLoader(ds_te_c, batch_size=64)
# Comparar estratégias de fine-tuning
estrategias_ft = {
"Linear Probing": True,
"Full Fine-tuning": False,
}
resultados_ft = {}
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
for (nome, congelar), cor in zip(
estrategias_ft.items(),
["steelblue", "coral"]
):
print(f"\n{'='*40}")
bert_ft = BERTMiniatura(vocab_size=1000, d_model=128,
n_heads=4, n_camadas=4)
modelo_ft = BERTParaClassificacao(
bert_ft, n_classes=3, congelar_bert=congelar
).to(device)
opt_ft = torch.optim.AdamW(
filter(lambda p: p.requires_grad,
modelo_ft.parameters()),
lr=2e-4 if congelar else 2e-5
)
crit_ft = nn.CrossEntropyLoss()
hist_ft = {"acc_tr": [], "acc_va": []}
for epoca in range(20):
modelo_ft.train()
n_c, n_t = 0, 0
for x_b, y_b in dl_tr_c:
x_b = x_b.to(device)
y_b = y_b.to(device)
opt_ft.zero_grad()
logits = modelo_ft(x_b)
loss = crit_ft(logits, y_b)
loss.backward()
opt_ft.step()
n_c += (logits.argmax(1) == y_b).sum().item()
n_t += len(y_b)
hist_ft["acc_tr"].append(n_c / n_t)
modelo_ft.eval()
n_c, n_t = 0, 0
with torch.no_grad():
for x_b, y_b in dl_va_c:
x_b = x_b.to(device)
y_b = y_b.to(device)
logits = modelo_ft(x_b)
n_c += (logits.argmax(1) == y_b).sum().item()
n_t += len(y_b)
hist_ft["acc_va"].append(n_c / n_t)
# Teste
modelo_ft.eval()
n_c, n_t = 0, 0
with torch.no_grad():
for x_b, y_b in dl_te_c:
x_b = x_b.to(device)
y_b = y_b.to(device)
logits = modelo_ft(x_b)
n_c += (logits.argmax(1) == y_b).sum().item()
n_t += len(y_b)
acc_te_ft = n_c / n_t
resultados_ft[nome] = acc_te_ft
print(f"\n {nome} — Acurácia no Teste: {acc_te_ft:.4f}")
ep = range(1, 21)
axes[0].plot(ep, hist_ft["acc_tr"],
label=f"{nome} (tr)", color=cor,
linewidth=2, alpha=0.6)
axes[1].plot(ep, hist_ft["acc_va"],
label=f"{nome} acc={acc_te_ft:.3f}",
color=cor, linewidth=2)
for ax, titulo in zip(axes,
["Acurácia Treino",
"Acurácia Validação"]):
ax.set_xlabel("Época")
ax.set_ylabel("Acurácia")
ax.set_title(titulo)
ax.legend(fontsize=9)
ax.set_ylim(0, 1)
plt.suptitle("Fine-tuning: Linear Probing vs Full Fine-tuning",
fontsize=13)
plt.tight_layout()
plt.show()
8. LoRA — Low-Rank Adaptation
class LoRALinear(nn.Module):
"""
Implementação do LoRA para uma camada Linear.
Ideia: em vez de atualizar W (d × d), aprende duas matrizes
de baixo rank: A (d × r) e B (r × d), onde r << d.
ΔW = BA → W_efetivo = W₀ + (α/r) × BA
Parâmetros adicionados: d×r + r×d = 2dr
Para d=768, r=8: 2×768×8 = 12.288 vs 768² = 589.824
Redução: ~98% menos parâmetros!
"""
def __init__(self,
linear_original: nn.Linear,
rank: int = 4,
alpha: float = 1.0,
dropout: float = 0.0):
super().__init__()
self.linear = linear_original
self.rank = rank
self.alpha = alpha
self.escala = alpha / rank
in_features = linear_original.in_features
out_features = linear_original.out_features
# Matrizes de baixo rank (treináveis)
self.lora_A = nn.Linear(in_features, rank,
bias=False)
self.lora_B = nn.Linear(rank, out_features,
bias=False)
self.dropout = nn.Dropout(dropout)
# Inicialização: A com Gaussian, B com zeros
# Garante que ΔW = BA = 0 no início
nn.init.kaiming_uniform_(self.lora_A.weight,
a=math.sqrt(5))
nn.init.zeros_(self.lora_B.weight)
# Congelar pesos originais
for param in self.linear.parameters():
param.requires_grad = False
def forward(self, x: torch.Tensor) -> torch.Tensor:
# Saída original (pesos congelados)
saida_original = self.linear(x)
# Adaptação de baixo rank (pesos treináveis)
adaptacao = self.lora_B(self.lora_A(
self.dropout(x)
)) * self.escala
return saida_original + adaptacao
@property
def n_params_treinaveis(self) -> int:
return (sum(p.numel() for p in self.lora_A.parameters())
+ sum(p.numel() for p in self.lora_B.parameters()))
def aplicar_lora(modelo: nn.Module,
rank: int = 4,
alpha: float = 1.0,
modulos_alvo: list = None) -> nn.Module:
"""
Aplica LoRA a todas as camadas Linear do modelo.
"""
if modulos_alvo is None:
modulos_alvo = ["W_Q", "W_V"] # típico do LoRA
for nome, modulo in list(modelo.named_modules()):
if isinstance(modulo, nn.Linear):
# Verificar se é um módulo alvo
nome_curto = nome.split(".")[-1]
if modulos_alvo is None or \
any(alvo in nome for alvo in modulos_alvo):
# Substituir pela versão LoRA
parent = modelo
partes = nome.split(".")
for parte in partes[:-1]:
parent = getattr(parent, parte)
setattr(parent, partes[-1],
LoRALinear(modulo, rank, alpha))
return modelo
# Demonstração do LoRA
print("\nLoRA — Low-Rank Adaptation:")
print("=" * 50)
# Criar modelo base
bert_base = BERTMiniatura(
vocab_size=1000, d_model=128, n_heads=4, n_camadas=2
)
n_params_base = sum(
p.numel() for p in bert_base.parameters()
)
# Congelar todo o modelo base
for param in bert_base.parameters():
param.requires_grad = False
# Aplicar LoRA
bert_lora = aplicar_lora(
bert_base, rank=4, alpha=8.0,
modulos_alvo=["W_Q", "W_K", "W_V"]
)
# Adicionar cabeça de classificação
cabeca_lora = nn.Linear(128, 3)
bert_lora_completo = nn.Sequential(
bert_lora, cabeca_lora
)
n_params_lora = sum(
p.numel() for p in bert_lora.parameters()
if p.requires_grad
)
n_params_cabeca = sum(
p.numel() for p in cabeca_lora.parameters()
)
n_treinaveis_total = n_params_lora + n_params_cabeca
print(f" Parâmetros totais do modelo base: {n_params_base:,}")
print(f" Parâmetros treináveis (LoRA + cabeça): "
f"{n_treinaveis_total:,}")
print(f" Redução: {1 - n_treinaveis_total/n_params_base:.1%} "
f"menos parâmetros treináveis")
print(f"\n Matrizes LoRA adicionadas:")
for nome, modulo in bert_lora.named_modules():
if isinstance(modulo, LoRALinear):
print(f" {nome}: {modulo.n_params_treinaveis:,} params "
f"(rank={modulo.rank})")
9. Perplexidade e Métricas de LLMs
print("\n" + "=" * 60)
print("MÉTRICAS DE AVALIAÇÃO DE LLMs")
print("=" * 60)
print("""
Métricas principais para modelos de linguagem:
Perplexidade (PPL):
PPL = exp(média das log-probs negativas)
Interpretação: "quantas palavras o modelo considera igualmente
prováveis a cada passo"
PPL = 1 → modelo perfeito (prever sempre a palavra certa)
PPL = 10 → como se escolhesse entre 10 palavras igualmente
PPL = 100 → muito incerto
BPC/BPB (bits per character/byte):
Medida de compressão — modelo bom = comprime mais
BLEU (para tradução/geração):
Mede sobreposição de n-gramas com referências humanas
ROUGE (para sumarização):
Recall-oriented de n-gramas
Benchmarks de capabilities:
MMLU: 57 tarefas acadêmicas (medicina, direito, etc)
HellaSwag: completamento de sentenças
HumanEval: geração de código Python
GSM8K: problemas matemáticos
TruthfulQA: respostas factuais precisas
""")
def calcular_perplexidade(modelo: GPTMiniatura,
dataloader,
device: torch.device) -> float:
"""Calcula a perplexidade no dataset."""
modelo.eval()
total_loss = 0.0
total_tokens = 0
crit = nn.CrossEntropyLoss(reduction="sum")
with torch.no_grad():
for x_b, y_b in dataloader:
x_b = x_b.to(device)
y_b = y_b.to(device)
logits = modelo(x_b)
loss = crit(
logits.view(-1, modelo.vocab_size),
y_b.view(-1)
)
total_loss += loss.item()
total_tokens += y_b.numel()
avg_loss = total_loss / total_tokens
perplexidade = math.exp(avg_loss)
return perplexidade
# Calcular perplexidade do GPT treinado
dl_lm_test = torch.utils.data.DataLoader(
LMDataset(n=500, seq_len=32, vocab_size=200),
batch_size=64
)
ppl = calcular_perplexidade(gpt_lm, dl_lm_test, device)
print(f"\nPerplexidade do GPT miniatura no teste: {ppl:.2f}")
print(f"(Menor = melhor; random baseline ≈ {200:.0f})")
# Visualizar evolução da perplexidade durante treino
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ep_lm = range(1, len(hist_lm) + 1)
axes[0].plot(ep_lm, hist_lm, color="steelblue",
linewidth=2)
axes[0].set_xlabel("Época")
axes[0].set_ylabel("Cross-Entropy Loss")
axes[0].set_title("Treinamento do GPT por LM")
ppls = [math.exp(l) for l in hist_lm]
axes[1].plot(ep_lm, ppls, color="coral", linewidth=2)
axes[1].set_xlabel("Época")
axes[1].set_ylabel("Perplexidade")
axes[1].set_title("Perplexidade ao Longo do Treino\n"
"(Menor = modelo mais certo)")
plt.tight_layout()
plt.show()
10. Exemplo Completo: Usando LLMs na Prática
print("\n" + "=" * 60)
print("USANDO LLMs NA PRÁTICA — SIMULAÇÃO COMPLETA")
print("=" * 60)
# Pipeline completo: pré-treino → fine-tuning → inferência
print("""
Pipeline completo para uma tarefa de NLP:
1. Escolher o modelo base:
Encoder-only → classificação, NER, QA extrativa
Decoder-only → geração, completamento
Encoder-Decoder → tradução, sumarização
2. Pré-processamento:
Tokenizar com o tokenizer do modelo
Truncar/paddar para comprimento máximo
Adicionar tokens especiais
3. Fine-tuning:
Full: melhor perf., mais recurso
LoRA: padrão para modelos grandes
Linear Probing: baseline rápido
4. Avaliação:
Métricas da tarefa (F1, BLEU, etc.)
Perplexidade para geração
Testes humanos para qualidade
5. Inferência:
Batch inference para throughput
Quantização para reduzir memória
""")
# Simulação de classificação com BERT fine-tuned
class PipelineClassificacaoBERT:
"""
Pipeline completo de classificação com BERT.
Simula o que você faria com Hugging Face na prática.
"""
def __init__(self,
vocab_size: int = 1000,
n_classes: int = 3,
nomes_classes: list = None):
self.vocab_size = vocab_size
self.n_classes = n_classes
self.nomes_classes = nomes_classes or \
[f"Classe {i}" for i in range(n_classes)]
# Modelo
bert_interno = BERTMiniatura(
vocab_size=vocab_size,
d_model=128, n_heads=4, n_camadas=4
)
self.modelo = BERTParaClassificacao(
bert_interno, n_classes=n_classes,
congelar_bert=False
).to(device)
self.optimizer = torch.optim.AdamW(
self.modelo.parameters(), lr=2e-5
)
self.criterion = nn.CrossEntropyLoss()
self.historico = {"loss": [], "acc": []}
def treinar(self, dataloader,
dataloader_val, n_epocas: int = 10):
melhor = 0.0
for epoca in range(n_epocas):
self.modelo.train()
n_c, n_t = 0, 0
for x_b, y_b in dataloader:
x_b = x_b.to(device)
y_b = y_b.to(device)
self.optimizer.zero_grad()
logits = self.modelo(x_b)
loss = self.criterion(logits, y_b)
loss.backward()
nn.utils.clip_grad_norm_(
self.modelo.parameters(), 1.0
)
self.optimizer.step()
n_c += (logits.argmax(1) == y_b).sum().item()
n_t += len(y_b)
self.modelo.eval()
n_cv, n_tv = 0, 0
with torch.no_grad():
for x_b, y_b in dataloader_val:
x_b = x_b.to(device)
y_b = y_b.to(device)
logits = self.modelo(x_b)
n_cv += (logits.argmax(1) == y_b).sum().item()
n_tv += len(y_b)
acc_va = n_cv / n_tv
self.historico["loss"].append(
self.criterion(logits, y_b).item()
)
self.historico["acc"].append(acc_va)
if acc_va > melhor:
melhor = acc_va
torch.save(self.modelo.state_dict(),
"bert_pipeline.pt")
self.modelo.load_state_dict(
torch.load("bert_pipeline.pt", weights_only=True)
)
return self
@torch.no_grad()
def predizer(self, tokens: torch.Tensor) -> dict:
"""Retorna predição e probabilidades."""
self.modelo.eval()
tokens = tokens.to(device)
logits = self.modelo(tokens)
probs = F.softmax(logits, dim=-1)
pred = probs.argmax(dim=-1)
return {
"classe": [self.nomes_classes[p.item()]
for p in pred],
"probabilidades": probs.cpu().tolist(),
"confianca": probs.max(dim=-1).values.cpu().tolist()
}
# Instanciar e treinar pipeline
pipeline = PipelineClassificacaoBERT(
vocab_size=1000, n_classes=3,
nomes_classes=["Negativo", "Neutro", "Positivo"]
)
print(f"\nTreinando pipeline BERT...")
pipeline.treinar(dl_tr_c, dl_va_c, n_epocas=15)
# Teste final
pipeline.modelo.eval()
n_c, n_t = 0, 0
todos_preds, todos_labels = [], []
with torch.no_grad():
for x_b, y_b in dl_te_c:
resultado = pipeline.predizer(x_b)
preds_idx = [
pipeline.nomes_classes.index(c)
for c in resultado["classe"]
]
n_c += sum(p == y for p, y in zip(
preds_idx, y_b.tolist()
))
n_t += len(y_b)
todos_preds.extend(preds_idx)
todos_labels.extend(y_b.tolist())
from sklearn.metrics import accuracy_score
acc_pipeline = accuracy_score(todos_labels, todos_preds)
print(f"Acurácia final do pipeline: {acc_pipeline:.4f}")
# Visualização final
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# Curva de acurácia
ep = range(1, len(pipeline.historico["acc"]) + 1)
axes[0].plot(ep, pipeline.historico["acc"],
color="steelblue", linewidth=2)
axes[0].axhline(y=acc_pipeline, color="coral",
linestyle="--", label=f"Teste={acc_pipeline:.3f}")
axes[0].set_xlabel("Época")
axes[0].set_ylabel("Acurácia Validação")
axes[0].set_title("Fine-tuning BERT")
axes[0].legend()
# Comparação de modelos
modelos_cmp = {
"Linear Probing": resultados_ft.get("Linear Probing", 0),
"Full FT": resultados_ft.get("Full Fine-tuning", 0),
"Pipeline BERT": acc_pipeline,
}
cores_cmp = ["coral", "steelblue", "mediumseagreen"]
barras = axes[1].bar(
modelos_cmp.keys(),
modelos_cmp.values(),
color=cores_cmp, alpha=0.85
)
axes[1].set_ylim(0, 1.1)
axes[1].set_title("Comparação de Estratégias")
axes[1].set_ylabel("Acurácia no Teste")
for b, v in zip(barras, modelos_cmp.values()):
axes[1].text(b.get_x() + b.get_width()/2,
v + 0.02, f"{v:.4f}",
ha="center", fontsize=10)
# Resumo do LoRA
lora_dados = {
"Modelo Base": n_params_base,
"LoRA Treináveis": n_treinaveis_total,
}
cores_lora = ["coral", "steelblue"]
barras_lora = axes[2].bar(
lora_dados.keys(),
lora_dados.values(),
color=cores_lora, alpha=0.85
)
axes[2].set_title("Eficiência do LoRA\n"
"Parâmetros treináveis")
axes[2].set_ylabel("Número de parâmetros")
for b, v in zip(barras_lora, lora_dados.values()):
axes[2].text(b.get_x() + b.get_width()/2,
v * 1.02, f"{v:,}",
ha="center", fontsize=9)
plt.suptitle("LLMs na Prática: Fine-tuning e Eficiência",
fontsize=13)
plt.tight_layout()
plt.show()
print("\n" + "=" * 60)
print("RESUMO: DO PRÉ-TREINAMENTO AO DEPLOY")
print("=" * 60)
print("""
Fluxo completo de um projeto com LLMs:
1. Escolher modelo base (BERT, GPT, T5, LLaMA...)
2. Carregar via Hugging Face (próxima aula!)
3. Tokenizar dados com o tokenizer do modelo
4. Fine-tuning (LoRA para eficiência)
5. Avaliar com métricas da tarefa
6. Servir via API (FastAPI, Gradio, etc.)
Considerações práticas:
- Modelos > 7B parâmetros precisam de GPU com ≥ 16GB VRAM
- Quantização (4-bit, 8-bit) reduz uso de memória em 4-8x
- LoRA é o padrão atual para fine-tuning eficiente
- Modelos fechados (GPT-4) via API, modelos abertos via HF
""")
Resumo da Aula
- BERT é encoder-only, bidirecional, pré-treinado com MLM (prever tokens mascarados) e NSP
- GPT é decoder-only, causal, pré-treinado com Language Modeling (prever próximo token)
- BERT é superior para compreensão; GPT é superior para geração
- O pré-treinamento em bilhões de tokens cria representações ricas transferíveis para qualquer tarefa
- Fine-tuning adapta o LLM: Linear Probing (apenas cabeça), Full Fine-tuning (tudo), LoRA (eficiente)
- LoRA reduz parâmetros treináveis em >95% mantendo performance próxima do Full Fine-tuning
- Perplexidade mede a incerteza do modelo: PPL = exp(loss), menor é melhor
- RLHF (Reinforcement Learning from Human Feedback) alinha modelos como ChatGPT a preferências humanas
- A próxima aula usará Hugging Face para carregar e usar modelos reais como BERT e GPT-2
Exercícios
-
Explique o objetivo da tarefa MLM (Masked Language Modeling) no BERT. Por que 15% dos tokens são mascarados e não 50% ou 5%? Por que usar a estratégia 80/10/10 (mask/random/original)?
✓ Resposta:MLM transforma o problema de representação de linguagem em uma tarefa supervisionada: dado um texto com alguns tokens ocultos, reconstrua-os usando o contexto bidirecional completo. Isso força o modelo a aprender representações que capturam semântica e sintaxe.
15% é um equilíbrio empírico. Com 5%, muito poucos tokens são preditos por época — o aprendizado é lento e ineficiente. Com 50%, o texto fica tão distorcido que o contexto disponível é insuficiente para fazer predições coerentes — a tarefa fica muito difícil e o modelo aprende representações ruins.
A estratégia 80/10/10 resolve um problema de incompatibilidade entre pré-treinamento e fine-tuning. Se todos os tokens mascarados fossem substituídos por [MASK], o modelo nunca veria tokens reais durante o pré-treinamento nessas posições. No fine-tuning, não há [MASK] — o modelo veria apenas tokens reais. Essa discrepância prejudicaria a performance. Os 10% com token aleatório forçam o modelo a contextualizar todo token e não apenas os [MASK]. Os 10% mantidos originais ensinam que a resposta pode ser o próprio token da posição.
-
O que é geração autoregressiva no GPT? Explique o processo token-por-token. Por que não podemos gerar todos os tokens em paralelo como fazemos no encoder?
✓ Resposta:Geração autoregressiva significa que o GPT gera um token por vez, usando todos os tokens gerados anteriormente como contexto para o próximo. O processo é:
- Receber o prompt: "O gato"
- Calcular probabilidades para cada token do vocabulário dado o contexto
- Amostrar um token (ex: "sentou") de acordo com essas probabilidades
- Adicionar ao contexto: "O gato sentou"
- Repetir com o novo contexto até atingir o comprimento máximo ou um token de fim
Não podemos gerar todos os tokens em paralelo porque cada token depende dos anteriores. Na aula 25, vimos que o decoder usa máscara causal — durante o treino, o token na posição t não pode ver t+1. Portanto, durante a inferência, o token t+1 genuinamente não existe ainda quando t está sendo calculado. O BERT pode processar em paralelo porque ele nunca gera — ele apenas processa sequências existentes.
-
O que é temperatura na geração de texto? Explique o efeito de temperatura = 0.1, 1.0 e 2.0 em termos de criatividade e coerência. Quando você usaria cada valor?
✓ Resposta:A temperatura escala os logits antes do softmax: logits_escalados = logits / temperatura. Isso controla a "nitidez" da distribuição de probabilidades.
Temperatura = 0.1: distribui quase toda a probabilidade no token mais provável. O modelo é muito determinístico e conservador — sempre escolhe a opção mais segura. Útil para: code generation, geração de fatos, tarefas onde a precisão importa mais que a criatividade.
Temperatura = 1.0: usa a distribuição original do modelo, sem modificação. Equilíbrio entre criatividade e coerência. É o padrão e funciona bem para a maioria das tarefas.
Temperatura = 2.0: "achata" a distribuição — tokens improváveis recebem probabilidade relativamente maior. O modelo é muito criativo/aleatório, podendo gerar textos incoerentes ou surpreendentes. Útil para: brainstorming, geração de poesia experimental, diversidade na geração. Raramente usada em produção.
-
Explique o LoRA matematicamente. Por que funciona? Quais são os parâmetros
rankealphae como eles afetam o tradeoff entre eficiência e performance?✓ Resposta:O LoRA parte da observação empírica de que os updates de peso durante fine-tuning têm baixa dimensionalidade intrínseca. Em vez de aprender ΔW (d × d = d² parâmetros), aprende duas matrizes: A (d × r) e B (r × d), onde r << d. O update efetivo é ΔW = BA.
Isso funciona porque modelos pré-treinados já aprenderam representações ricas. O fine-tuning precisa fazer apenas pequenos ajustes nessas representações, e esses ajustes têm estrutura de baixo rank — podem ser capturados por poucas direções principais no espaço de parâmetros.
Parâmetro
rank(r): controla a "capacidade" do adaptador. r=4 é conservador, r=16 ou r=32 é mais expressivo. Rank baixo: menos parâmetros, pode não capturar todas as nuances do fine-tuning. Rank alto: mais expressivo, mais parâmetros, maior risco de overfitting.Parâmetro
alpha(α): escala o update pelo fator α/r. Separa o learning rate do rank — permite usar o mesmo learning rate independente do rank escolhido. Convenção comum: alpha = 2 × rank. -
O que é perplexidade em modelos de linguagem? Se um modelo tem PPL=10 em inglês e PPL=50 em swahili, o que isso indica? Por que a perplexidade é uma métrica mais informativa que a acurácia para LLMs?
✓ Resposta:Perplexidade = exp(perda de entropia cruzada média por token). Intuitivamente, é "quantas palavras o modelo considera igualmente prováveis a cada passo". PPL=10 significa que o modelo tem incerteza equivalente a escolher uniformemente entre 10 palavras.
PPL=10 em inglês e PPL=50 em swahili indica que o modelo foi treinado predominantemente em inglês. Para inglês, o modelo aprendeu bem a estrutura e pode fazer predições confiantes. Para swahili, viu poucos dados e tem maior incerteza — considera mais palavras igualmente prováveis a cada passo. Isso não significa que o modelo "erra mais" em swahili, mas que é menos confiante nas suas predições.
A perplexidade é mais informativa que acurácia para LLMs porque modelos de linguagem geram distribuições sobre o vocabulário inteiro, não classificam em classes fixas. A acurácia exigiria definir o que é "correto" — mas muitos tokens são semanticamente equivalentes em um dado contexto. A perplexidade captura quão bem o modelo comprime/prediz o texto completo, o que é uma medida mais natural da qualidade de representação da linguagem.
-
Compare os paradigmas de uso de LLMs: fine-tuning vs prompting (in-context learning). Quais são as vantagens e desvantagens de cada abordagem? Em qual situação você escolheria cada uma?
✓ Resposta:Fine-tuning adapta os pesos do modelo para uma tarefa específica usando exemplos rotulados. Vantagens: performance máxima na tarefa alvo, comportamento previsível e controlado, não depende da qualidade do prompt. Desvantagens: requer exemplos rotulados (que podem ser caros), requer compute para treino, um modelo fine-tuned por tarefa.
In-context learning (prompting) usa exemplos no próprio prompt sem atualizar os pesos. Vantagens: zero compute de treino, um único modelo para múltiplas tarefas, fácil de iterar rapidamente, funciona para tarefas sem dados de treino. Desvantagens: exemplos ocupam espaço do contexto (limitando input real), performance geralmente abaixo do fine-tuning, sensível à formulação do prompt.
Escolha fine-tuning quando: a tarefa é crítica e requer performance máxima, você tem ≥ 1000 exemplos rotulados de qualidade, a tarefa tem formato específico diferente do pré-treinamento, vai fazer muitas inferências (amortiza o custo do treino).
Escolha prompting quando: poucos ou nenhum exemplo rotulado disponível, precisa de prototipagem rápida, a tarefa muda frequentemente, está usando APIs de modelos fechados (GPT-4) onde não pode fazer fine-tuning facilmente.
Referências
- BERT paper (Devlin et al., 2018)
- GPT-1 paper (Radford et al., 2018)
- GPT-2 paper (Radford et al., 2019)
- GPT-3 paper (Brown et al., 2020)
- LoRA paper (Hu et al., 2021)
- Jay Alammar — The Illustrated BERT
- Jay Alammar — The Illustrated GPT-2
- Hugging Face — BERT documentation
- Hugging Face — GPT-2 documentation
- Andrej Karpathy — NanoGPT