Objetivo
Construir uma aplicação NLP completa do início ao fim, integrando todos os conceitos do curso: pré-processamento, embeddings, modelos pré-treinados, fine-tuning com LoRA, avaliação rigorosa e deploy com interface interativa. Este é o projeto que demonstra sua competência em IA aplicada.
1. Visão Geral do Projeto
O projeto é um Sistema Inteligente de Análise de Feedback de Clientes que:
- Classifica sentimentos (positivo/neutro/negativo)
- Detecta tópicos automaticamente
- Extrai aspectos avaliados (produto, entrega, suporte)
- Gera resumos das avaliações
- Fornece dashboard analítico
- Expõe API REST para integração
Este tipo de sistema tem valor direto em negócios reais e demonstra domínio completo da stack de NLP.
2. Arquitetura do Sistema
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import seaborn as sns
from torch.utils.data import Dataset, DataLoader
from collections import Counter, defaultdict
from dataclasses import dataclass, field
from typing import Optional
import math
import json
import time
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}")
print("=" * 65)
print("SISTEMA DE ANÁLISE DE FEEDBACK DE CLIENTES")
print("=" * 65)
print("""
Arquitetura do sistema:
┌─────────────────────────────────────────────────────────┐
│ PIPELINE DE ENTRADA │
│ Raw Text → Normalização → Tokenização → Embeddings │
└────────────────────────────┬────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌──────────────────┐
│ Classificador│ │ Extrator │ │ Sumarizador │
│ Sentimentos │ │ Aspectos │ │ (LLM lite) │
│ (3 classes) │ │ (Multi-tag) │ │ (geração) │
└───────┬───────┘ └───────┬───────┘ └────────┬─────────┘
└─────────────────────┼─────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ AGREGAÇÃO E ANÁLISE │
│ Métricas Globais → Dashboard → Exportação de Relatório │
└─────────────────────────────────────────────────────────┘
""")
3. Geração do Dataset de Feedback
@dataclass
class FeedbackCliente:
"""Estrutura de dados para um feedback de cliente."""
id: int
texto: str
produto: str
sentimento: str # positivo / neutro / negativo
aspectos: list # lista de aspectos avaliados
nota: int # 1-5
data: str
util: bool = True
def to_dict(self) -> dict:
return {
"id": self.id, "texto": self.texto,
"produto": self.produto,
"sentimento": self.sentimento,
"aspectos": self.aspectos,
"nota": self.nota, "data": self.data
}
class GeradorFeedback:
"""
Gera dataset sintético realista de feedback de clientes.
"""
PRODUTOS = [
"Smartphone XR Pro", "Notebook Ultra",
"Fone Bluetooth", "Smart Watch",
"Tablet Premium"
]
TEMPLATES = {
"positivo": [
"Excelente {produto}! Chegou antes do prazo e {qualidade}. Super recomendo!",
"Amei o {produto}! {qualidade}. A entrega foi {entrega} e o suporte foi ótimo.",
"Produto de altíssima qualidade! O {produto} {qualidade}. Preço justo.",
"Muito satisfeito com o {produto}. {qualidade}. Voltarei a comprar!",
"Superou todas as expectativas! {qualidade}. Entrega {entrega}.",
],
"neutro": [
"O {produto} é razoável pelo preço. {qualidade}, mas poderia melhorar.",
"Produto ok. O {produto} {qualidade}. Entrega {entrega}. Nada especial.",
"Dentro do esperado. {qualidade}. O {produto} cumpre sua função.",
"Regular. O {produto} {qualidade}, mas esperava mais por esse valor.",
"Nem bom nem ruim. {qualidade}. Cumpre o prometido, sem mais.",
],
"negativo": [
"Decepcionante! O {produto} {qualidade_neg}. Não recomendo.",
"Produto com defeito. O {produto} {qualidade_neg}. Péssimo suporte.",
"Muito abaixo do esperado. {qualidade_neg}. Entrega {entrega_neg}.",
"Não valeu o preço. O {produto} {qualidade_neg}. Devolvi.",
"Terrível experiência! {qualidade_neg}. O suporte não resolveu nada.",
]
}
QUALIDADES_POS = [
"funciona perfeitamente", "bateria dura o dia todo",
"tela muito nítida", "muito rápido e responsivo",
"acabamento premium", "build quality excelente",
"desempenho surpreendente", "câmera excelente"
]
QUALIDADES_NEU = [
"funciona mas sem destaque", "bateria durou o esperado",
"qualidade mediana", "desempenho ok para o preço"
]
QUALIDADES_NEG = [
"parou de funcionar em 2 semanas",
"veio com defeito de fábrica",
"bateria dura apenas 2 horas",
"tela apresentou manchas",
"travou logo nos primeiros dias"
]
ENTREGAS_POS = ["rápida", "antecipada", "dentro do prazo"]
ENTREGAS_NEG = ["atrasou 2 semanas", "veio danificado",
"errada"]
ASPECTOS_MAP = {
"produto": ["funciona", "qualidade", "acabamento",
"desempenho", "defeito", "câmera",
"bateria", "tela"],
"entrega": ["chegou", "prazo", "atrasou", "entrega",
"danificado"],
"suporte": ["suporte", "atendimento", "resolveu",
"devolver", "trocar"],
"preco": ["preço", "valor", "caro", "barato",
"custo"],
}
def gerar(self, n: int = 1000,
seed: int = 42) -> list:
np.random.seed(seed)
feedbacks = []
distribuicao = {"positivo": 0.55, "neutro": 0.25,
"negativo": 0.20}
sentimentos = list(distribuicao.keys())
probs = list(distribuicao.values())
datas = pd.date_range("2024-01-01", "2024-12-31",
periods=n)
for i in range(n):
sentimento = np.random.choice(sentimentos, p=probs)
produto = np.random.choice(self.PRODUTOS)
# Gerar texto baseado no template
template = np.random.choice(
self.TEMPLATES[sentimento]
)
qualidade = np.random.choice(
self.QUALIDADES_POS if sentimento == "positivo"
else self.QUALIDADES_NEU if sentimento == "neutro"
else self.QUALIDADES_NEG
)
entrega = np.random.choice(
self.ENTREGAS_POS if sentimento != "negativo"
else self.ENTREGAS_NEG
)
texto = template.format(
produto=produto,
qualidade=qualidade,
qualidade_neg=qualidade,
entrega=entrega
)
# Detectar aspectos mencionados
texto_lower = texto.lower()
aspectos = []
for aspecto, palavras in self.ASPECTOS_MAP.items():
if any(p in texto_lower for p in palavras):
aspectos.append(aspecto)
if not aspectos:
aspectos = ["produto"]
# Nota correlacionada com sentimento
if sentimento == "positivo":
nota = np.random.choice([4, 5],
p=[0.3, 0.7])
elif sentimento == "neutro":
nota = np.random.choice([2, 3, 4],
p=[0.2, 0.6, 0.2])
else:
nota = np.random.choice([1, 2],
p=[0.7, 0.3])
feedbacks.append(FeedbackCliente(
id=i+1, texto=texto,
produto=produto,
sentimento=sentimento,
aspectos=aspectos,
nota=nota,
data=str(datas[i].date())
))
return feedbacks
# Gerar dataset
gerador = GeradorFeedback()
dataset = gerador.gerar(n=2000)
# Estatísticas do dataset
df_stats = pd.DataFrame([f.to_dict() for f in dataset])
print(f"\nDataset gerado: {len(dataset)} feedbacks")
print(f"\nDistribuição de sentimentos:")
print(df_stats["sentimento"].value_counts(normalize=True).mul(100).round(1))
print(f"\nDistribuição de notas:")
print(df_stats["nota"].value_counts().sort_index())
print(f"\nProdutos mais avaliados:")
print(df_stats["produto"].value_counts().head())
4. Módulo de Pré-processamento
class Preprocessador:
"""
Pipeline de pré-processamento de texto.
"""
# Vocabulário especial
ESPECIAIS = {
"<PAD>": 0, "<UNK>": 1, "<BOS>": 2,
"<EOS>": 3, "<MASK>": 4,
}
def __init__(self, max_vocab: int = 3000,
max_len: int = 64):
self.max_vocab = max_vocab
self.max_len = max_len
self.token2id = dict(self.ESPECIAIS)
self.id2token = {v: k for k, v in self.ESPECIAIS.items()}
self.freq = Counter()
self.ajustado = False
def _normalizar(self, texto: str) -> str:
"""Normalização básica de texto."""
import re
texto = texto.lower()
texto = re.sub(r"[!?.]+", lambda m: " " + m.group(0)[0] + " ", texto)
texto = re.sub(r"[,;:]", " ", texto)
texto = re.sub(r"\s+", " ", texto)
return texto.strip()
def _tokenizar(self, texto: str) -> list:
"""Tokenização por palavras."""
return self._normalizar(texto).split()
def ajustar(self, textos: list) -> "Preprocessador":
"""Aprende o vocabulário dos textos."""
for texto in textos:
self.freq.update(self._tokenizar(texto))
palavras_mais_freq = [
p for p, _ in
self.freq.most_common(self.max_vocab - len(self.ESPECIAIS))
]
for palavra in palavras_mais_freq:
idx = len(self.token2id)
self.token2id[palavra] = idx
self.id2token[idx] = palavra
self.ajustado = True
print(f"Vocabulário: {len(self.token2id):,} tokens")
return self
def transformar(self, texto: str) -> list:
"""Converte texto em sequência de IDs."""
tokens = self._tokenizar(texto)
ids = [self.token2id.get(t, 1) for t in tokens]
ids = ids[:self.max_len - 2]
ids = [2] + ids + [3] # BOS + tokens + EOS
ids += [0] * (self.max_len - len(ids)) # padding
return ids
def ajustar_transformar(self, textos: list) -> list:
self.ajustar(textos)
return [self.transformar(t) for t in textos]
# Criar e ajustar preprocessador
prep = Preprocessador(max_vocab=3000, max_len=64)
textos_treino = [f.texto for f in dataset]
prep.ajustar(textos_treino)
# Verificar tokenização
print(f"\nExemplos de tokenização:")
for feedback in dataset[:2]:
ids = prep.transformar(feedback.texto)
tokens_nao_pad = [prep.id2token.get(i, "<UNK>")
for i in ids if i != 0]
print(f" Texto: '{feedback.texto[:50]}...'")
print(f" IDs: {ids[:10]}...")
print(f" Tokens: {tokens_nao_pad[:8]}...")
print()
5. Módulo de Classificação de Sentimentos
class DatasetFeedback(Dataset):
"""Dataset para classificação de sentimentos."""
LABEL_MAP = {"positivo": 0, "neutro": 1, "negativo": 2}
LABEL_INV = {v: k for k, v in LABEL_MAP.items()}
def __init__(self, feedbacks: list,
preprocessador: Preprocessador):
self.dados = []
for f in feedbacks:
ids = preprocessador.transformar(f.texto)
label = self.LABEL_MAP[f.sentimento]
self.dados.append((ids, label))
def __len__(self):
return len(self.dados)
def __getitem__(self, idx):
ids, label = self.dados[idx]
return (torch.tensor(ids, dtype=torch.long),
torch.tensor(label, dtype=torch.long))
class ClassificadorSentimentos(nn.Module):
"""
Classificador de sentimentos baseado em Transformer.
Combina embeddings contextuais com atenção.
"""
def __init__(self,
vocab_size: int,
d_model: int = 128,
n_heads: int = 4,
n_camadas: int = 3,
d_ff: int = 256,
max_len: int = 64,
n_classes: int = 3,
dropout: float = 0.2):
super().__init__()
self.d_model = d_model
self.embedding = nn.Embedding(vocab_size, d_model,
padding_idx=0)
self.pos_emb = nn.Embedding(max_len, d_model)
self.dropout = nn.Dropout(dropout)
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model, nhead=n_heads,
dim_feedforward=d_ff, dropout=dropout,
batch_first=True, norm_first=True
)
self.encoder = nn.TransformerEncoder(
encoder_layer, num_layers=n_camadas,
norm=nn.LayerNorm(d_model)
)
# Cabeça de classificação com atenção
self.atencao_classe = nn.Linear(d_model, 1)
self.classificador = nn.Sequential(
nn.Linear(d_model, d_model // 2),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(d_model // 2, n_classes)
)
self._init_pesos()
def _init_pesos(self):
for p in self.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
def forward(self, x: torch.Tensor) -> tuple:
"""
x: (batch, seq_len)
Retorna: (logits, pesos_atencao)
"""
batch, seq_len = x.shape
pos = torch.arange(seq_len, device=x.device)
pos = pos.unsqueeze(0).expand(batch, -1)
# Máscara de padding
pad_mask = (x == 0)
# Embeddings
h = self.dropout(
self.embedding(x) * math.sqrt(self.d_model)
+ self.pos_emb(pos)
)
# Encoder
h = self.encoder(h, src_key_padding_mask=pad_mask)
# Atenção para pooling
scores = self.atencao_classe(h).squeeze(-1)
scores = scores.masked_fill(pad_mask, -1e9)
pesos = F.softmax(scores, dim=-1)
ctx = (pesos.unsqueeze(-1) * h).sum(dim=1)
logits = self.classificador(self.dropout(ctx))
return logits, pesos
# Preparar dados
from torch.utils.data import random_split
ds_completo = DatasetFeedback(dataset, prep)
n_tr = int(0.7 * len(ds_completo))
n_va = int(0.15 * len(ds_completo))
n_te = len(ds_completo) - n_tr - n_va
ds_tr, ds_va, ds_te = random_split(
ds_completo, [n_tr, n_va, n_te],
generator=torch.Generator().manual_seed(42)
)
dl_tr = DataLoader(ds_tr, batch_size=64, shuffle=True,
drop_last=True)
dl_va = DataLoader(ds_va, batch_size=64)
dl_te = DataLoader(ds_te, batch_size=64)
print(f"Divisão: Treino={n_tr}, Val={n_va}, Teste={n_te}")
# Instanciar e treinar
clf = ClassificadorSentimentos(
vocab_size=len(prep.token2id),
d_model=128, n_heads=4, n_camadas=3,
d_ff=256, max_len=64, n_classes=3,
dropout=0.2
).to(device)
n_clf = sum(p.numel() for p in clf.parameters())
print(f"\nClassificador: {n_clf:,} parâmetros")
# Treinamento
opt_clf = torch.optim.AdamW(clf.parameters(),
lr=2e-4, weight_decay=1e-3)
crit_clf = nn.CrossEntropyLoss()
sched_clf = torch.optim.lr_scheduler.OneCycleLR(
opt_clf, max_lr=5e-4,
steps_per_epoch=len(dl_tr),
epochs=25, pct_start=0.2
)
hist_clf = {"tr_loss": [], "va_loss": [],
"tr_acc": [], "va_acc": []}
melhor_va_clf = float("inf")
print(f"\nTreinando Classificador de Sentimentos...")
for epoca in range(25):
# Treino
clf.train()
losses_tr, n_c_tr, n_t_tr = [], 0, 0
for X_b, y_b in dl_tr:
X_b = X_b.to(device)
y_b = y_b.to(device)
opt_clf.zero_grad()
logits, _ = clf(X_b)
loss = crit_clf(logits, y_b)
loss.backward()
nn.utils.clip_grad_norm_(clf.parameters(), 1.0)
opt_clf.step()
sched_clf.step()
losses_tr.append(loss.item())
n_c_tr += (logits.argmax(1) == y_b).sum().item()
n_t_tr += len(y_b)
# Validação
clf.eval()
losses_va, n_c_va, n_t_va = [], 0, 0
with torch.no_grad():
for X_b, y_b in dl_va:
X_b = X_b.to(device)
y_b = y_b.to(device)
logits, _ = clf(X_b)
loss = crit_clf(logits, y_b)
losses_va.append(loss.item())
n_c_va += (logits.argmax(1) == y_b).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_clf["tr_loss"].append(loss_tr)
hist_clf["va_loss"].append(loss_va)
hist_clf["tr_acc"].append(acc_tr)
hist_clf["va_acc"].append(acc_va)
if loss_va < melhor_va_clf:
melhor_va_clf = loss_va
torch.save(clf.state_dict(), "clf_sentimentos.pt")
if (epoca + 1) % 5 == 0:
print(f" Época {epoca+1:2d}/25 | "
f"Loss: {loss_tr:.4f}/{loss_va:.4f} | "
f"Acc: {acc_tr:.4f}/{acc_va:.4f}")
# Avaliar no teste
clf.load_state_dict(torch.load("clf_sentimentos.pt",
weights_only=True))
clf.eval()
preds_te, labels_te = [], []
atencoes_exemplos = []
with torch.no_grad():
for X_b, y_b in dl_te:
X_b = X_b.to(device)
logits, pesos = clf(X_b)
preds_te.extend(logits.argmax(1).cpu().tolist())
labels_te.extend(y_b.tolist())
if len(atencoes_exemplos) < 3:
atencoes_exemplos.append({
"pesos": pesos[0].cpu().numpy(),
"pred": logits[0].argmax().item()
})
from sklearn.metrics import accuracy_score, classification_report
acc_clf = accuracy_score(labels_te, preds_te)
print(f"\nAcurácia no Teste: {acc_clf:.4f}")
print(f"\nRelatório Completo:")
print(classification_report(
labels_te, preds_te,
target_names=["Positivo", "Neutro", "Negativo"]
))
6. Módulo de Extração de Aspectos
class ExtratorAspectos:
"""
Extrator de aspectos baseado em regras e embeddings.
Identifica quais dimensões o cliente avaliou.
"""
REGRAS = {
"produto": [
"qualidade", "funciona", "defeito", "tela",
"bateria", "câmera", "desempenho", "rápido",
"acabamento", "build", "produto"
],
"entrega": [
"entrega", "chegou", "prazo", "atrasou",
"embalagem", "transporte", "frete", "rapido",
"danificado"
],
"suporte": [
"suporte", "atendimento", "contato", "devolver",
"troca", "garantia", "reclamação", "resolveu"
],
"preco": [
"preço", "valor", "caro", "barato", "custo",
"promoção", "desconto", "investimento"
],
}
def __init__(self):
# Matriz de co-ocorrência simplificada
self.co_matrix = defaultdict(Counter)
def ajustar(self, feedbacks: list) -> "ExtratorAspectos":
"""Aprende padrões de co-ocorrência."""
for feedback in feedbacks:
texto_lower = feedback.texto.lower()
for aspecto, palavras in self.REGRAS.items():
for palavra in palavras:
if palavra in texto_lower:
self.co_matrix[aspecto][palavra] += 1
return self
def extrair(self, texto: str) -> dict:
"""Extrai aspectos mencionados no texto."""
texto_lower = texto.lower()
resultado = {}
for aspecto, palavras in self.REGRAS.items():
matches = [p for p in palavras if p in texto_lower]
if matches:
# Score baseado na frequência aprendida
score = sum(
self.co_matrix[aspecto].get(p, 0.5)
for p in matches
)
resultado[aspecto] = {
"detectado": True,
"score": score,
"palavras": matches
}
else:
resultado[aspecto] = {
"detectado": False,
"score": 0.0,
"palavras": []
}
return resultado
def avaliar(self, feedbacks: list) -> dict:
"""Avalia o extrator contra os labels reais."""
acertos = defaultdict(int)
total = defaultdict(int)
for f in feedbacks:
pred = self.extrair(f.texto)
for aspecto in self.REGRAS:
pred_det = pred[aspecto]["detectado"]
real_det = aspecto in f.aspectos
total[aspecto] += 1
if pred_det == real_det:
acertos[aspecto] += 1
return {asp: acertos[asp] / total[asp]
for asp in self.REGRAS}
# Treinar e avaliar extrator
extrator = ExtratorAspectos()
extrator.ajustar(dataset)
acc_aspectos = extrator.avaliar(dataset[:500])
print("\nPrecisão do Extrator de Aspectos:")
for aspecto, acc in acc_aspectos.items():
barra = "█" * int(acc * 25)
print(f" {aspecto:<12}: {acc:.4f} {barra}")
7. Módulo de Sumarização
class SumarizadorFeedback:
"""
Sumarizador de feedback usando template + estatísticas.
Versão simplificada para demonstração.
"""
def sumarizar_grupo(self, feedbacks: list,
produto: str = None) -> str:
"""
Gera resumo textual de um grupo de feedbacks.
"""
if not feedbacks:
return "Nenhum feedback disponível."
n_total = len(feedbacks)
n_pos = sum(1 for f in feedbacks
if f.sentimento == "positivo")
n_neu = sum(1 for f in feedbacks
if f.sentimento == "neutro")
n_neg = sum(1 for f in feedbacks
if f.sentimento == "negativo")
nota_media = np.mean([f.nota for f in feedbacks])
pct_pos = n_pos / n_total * 100
pct_neg = n_neg / n_total * 100
# Aspectos mais mencionados
aspectos_freq = Counter()
for f in feedbacks:
for a in f.aspectos:
aspectos_freq[a] += 1
top_aspectos = aspectos_freq.most_common(2)
# Palavras mais frequentes no texto
todas_palavras = []
for f in feedbacks:
palavras = [p for p in f.texto.lower().split()
if len(p) > 4 and p not in
{"muito", "super", "seria", "desde",
"para", "esse", "este", "como"}]
todas_palavras.extend(palavras)
palavras_freq = Counter(todas_palavras).most_common(3)
palavras_str = ", ".join(
[f"'{p}'" for p, _ in palavras_freq]
)
# Tendência temporal
if len(feedbacks) > 10:
notas_rec = np.mean(
[f.nota for f in feedbacks[-5:]]
)
notas_ant = np.mean(
[f.nota for f in feedbacks[:5]]
)
tendencia = ("crescente" if notas_rec > notas_ant
else "decrescente" if notas_rec < notas_ant
else "estável")
else:
tendencia = "estável"
# Construir resumo
produto_str = f"para {produto} " if produto else ""
nivel_sat = ("excelente" if nota_media >= 4.5
else "bom" if nota_media >= 3.5
else "regular" if nota_media >= 2.5
else "ruim")
resumo = (
f"Análise de {n_total} avaliações {produto_str}"
f"(nota média: {nota_media:.1f}/5 — {nivel_sat}).\n\n"
f"Distribuição de sentimentos: {pct_pos:.0f}% positivos, "
f"{n_neu/n_total*100:.0f}% neutros, "
f"{pct_neg:.0f}% negativos.\n\n"
)
if top_aspectos:
asp_str = " e ".join(
[f"{a} ({c} menções)"
for a, c in top_aspectos]
)
resumo += f"Aspectos mais citados: {asp_str}.\n"
resumo += (
f"Termos mais frequentes: {palavras_str}.\n"
f"Tendência de satisfação: {tendencia}."
)
return resumo
sumarizador = SumarizadorFeedback()
resumo_geral = sumarizador.sumarizar_grupo(dataset)
print("\nResumo Geral do Dataset:")
print("-" * 55)
print(resumo_geral)
# Resumo por produto
print("\nResumo por Produto:")
for produto in GeradorFeedback.PRODUTOS[:2]:
feedbacks_produto = [f for f in dataset
if f.produto == produto]
resumo = sumarizador.sumarizar_grupo(
feedbacks_produto, produto
)
print(f"\n {produto}:")
print(f" " + resumo.replace("\n", "\n "))
8. Dashboard Analítico
def criar_dashboard(dataset: list,
hist_clf: dict,
acc_clf: float,
acc_aspectos: dict):
"""Cria dashboard completo do sistema."""
df = pd.DataFrame([f.to_dict() for f in dataset])
fig = plt.figure(figsize=(20, 16))
gs = gridspec.GridSpec(3, 3, figure=fig,
hspace=0.45, wspace=0.4)
# ─── 1. Distribuição de Sentimentos ──────────────────
ax1 = fig.add_subplot(gs[0, 0])
contagens = df["sentimento"].value_counts()
cores_sent = {"positivo": "#2ecc71",
"neutro": "#f39c12",
"negativo": "#e74c3c"}
ax1.pie(
contagens.values,
labels=contagens.index,
colors=[cores_sent[s] for s in contagens.index],
autopct="%1.1f%%",
startangle=90,
wedgeprops={"edgecolor": "white", "linewidth": 2}
)
ax1.set_title("Distribuição de\nSentimentos", fontsize=12)
# ─── 2. Notas por Produto ─────────────────────────────
ax2 = fig.add_subplot(gs[0, 1])
media_notas = df.groupby("produto")["nota"].mean().sort_values()
nomes_curtos = [p.split()[0] for p in media_notas.index]
barras = ax2.barh(nomes_curtos, media_notas.values,
color="steelblue", alpha=0.8)
ax2.set_xlim(1, 5)
ax2.axvline(x=media_notas.mean(), color="coral",
linestyle="--", linewidth=1.5,
label=f"Média={media_notas.mean():.2f}")
for b, v in zip(barras, media_notas.values):
ax2.text(v + 0.05, b.get_y() + b.get_height()/2,
f"{v:.2f}", va="center", fontsize=9)
ax2.set_title("Nota Média\npor Produto", fontsize=12)
ax2.legend(fontsize=8)
# ─── 3. Curva de Aprendizado ──────────────────────────
ax3 = fig.add_subplot(gs[0, 2])
ep = range(1, len(hist_clf["tr_acc"]) + 1)
ax3.plot(ep, hist_clf["tr_acc"], color="steelblue",
linewidth=2, label=f"Treino")
ax3.plot(ep, hist_clf["va_acc"], color="coral",
linewidth=2, linestyle="--",
label=f"Validação (teste={acc_clf:.3f})")
ax3.set_xlabel("Época")
ax3.set_ylabel("Acurácia")
ax3.set_title("Curva de Aprendizado\nClassificador", fontsize=12)
ax3.legend(fontsize=8)
ax3.set_ylim(0, 1.05)
# ─── 4. Precisão por Aspecto ─────────────────────────
ax4 = fig.add_subplot(gs[1, 0])
asp_nomes = list(acc_aspectos.keys())
asp_vals = list(acc_aspectos.values())
cores_asp = ["#27ae60" if v >= 0.8 else
"#f39c12" if v >= 0.6 else
"#e74c3c" for v in asp_vals]
barras_asp = ax4.bar(asp_nomes, asp_vals,
color=cores_asp, alpha=0.85)
ax4.set_ylim(0, 1.1)
ax4.set_title("Precisão por Aspecto\n(Extrator)", fontsize=12)
ax4.axhline(y=0.8, color="gray", linestyle=":",
alpha=0.7, label="Meta 80%")
ax4.legend(fontsize=8)
for b, v in zip(barras_asp, asp_vals):
ax4.text(b.get_x() + b.get_width()/2,
v + 0.02, f"{v:.2f}",
ha="center", fontsize=9)
# ─── 5. Sentimento por Produto ───────────────────────
ax5 = fig.add_subplot(gs[1, 1])
pivot_sent = pd.crosstab(df["produto"], df["sentimento"],
normalize="index") * 100
if not pivot_sent.empty:
produtos_curtos = [p.split()[0]
for p in pivot_sent.index]
bottom_vals = np.zeros(len(produtos_curtos))
for sent, cor in [("positivo", "#2ecc71"),
("neutro", "#f39c12"),
("negativo", "#e74c3c")]:
if sent in pivot_sent.columns:
vals = pivot_sent[sent].values
ax5.bar(produtos_curtos, vals, bottom=bottom_vals,
label=sent, color=cor, alpha=0.85)
bottom_vals += vals
ax5.set_title("Sentimento por\nProduto (%)", fontsize=12)
ax5.legend(fontsize=8, loc="upper right")
ax5.set_ylabel("Proporção (%)")
ax5.tick_params(axis="x", rotation=30)
# ─── 6. Evolução Temporal ────────────────────────────
ax6 = fig.add_subplot(gs[1, 2])
df["data"] = pd.to_datetime(df["data"])
df["mes"] = df["data"].dt.to_period("M")
evolucao = (df.groupby("mes")["nota"]
.mean().reset_index())
evolucao["mes_str"] = evolucao["mes"].astype(str)
ax6.plot(range(len(evolucao)),
evolucao["nota"].values,
color="steelblue", linewidth=2.5,
marker="o", markersize=4)
ax6.fill_between(range(len(evolucao)),
evolucao["nota"].values,
alpha=0.2, color="steelblue")
ax6.set_xticks(range(0, len(evolucao), 2))
ax6.set_xticklabels(
evolucao["mes_str"].iloc[::2],
rotation=45, fontsize=7
)
ax6.set_title("Evolução da Nota\nMédia Mensal", fontsize=12)
ax6.set_ylabel("Nota Média")
ax6.set_ylim(1, 5)
# ─── 7. Palavras mais frequentes por sentimento ──────
ax7 = fig.add_subplot(gs[2, :])
palavras_por_sent = {}
for sent in ["positivo", "negativo"]:
textos_sent = df[df["sentimento"] == sent]["texto"]
todas_p = []
for texto in textos_sent:
palavras = [p for p in texto.lower().split()
if len(p) > 4 and p not in
{"muito", "super", "seria",
"desde", "para", "esse",
"este", "como", "dentro",
"produto"}]
todas_p.extend(palavras)
palavras_por_sent[sent] = Counter(todas_p).most_common(10)
# Gráfico de palavras
x_pos = np.arange(10)
largura = 0.35
palavras_p = [p for p, _ in palavras_por_sent.get(
"positivo", []
)[:10]]
freqs_p = [f for _, f in palavras_por_sent.get(
"positivo", []
)[:10]]
palavras_n = [p for p, _ in palavras_por_sent.get(
"negativo", []
)[:10]]
freqs_n = [f for _, f in palavras_por_sent.get(
"negativo", []
)[:10]]
if palavras_p:
ax7.bar(x_pos - largura/2, freqs_p, largura,
label="Positivo", color="#2ecc71", alpha=0.8)
ax7.set_xticks(x_pos[:len(palavras_p)])
ax7.set_xticklabels(palavras_p[:len(x_pos)],
rotation=30, fontsize=9)
if palavras_n:
ax7.bar(x_pos + largura/2, freqs_n[:len(x_pos)],
largura, label="Negativo",
color="#e74c3c", alpha=0.8)
ax7.set_title("Top 10 Palavras por Sentimento",
fontsize=12)
ax7.set_ylabel("Frequência")
ax7.legend()
plt.suptitle("Dashboard: Sistema de Análise de Feedback",
fontsize=16, y=0.98, fontweight="bold")
plt.savefig("dashboard_feedback.png",
dpi=150, bbox_inches="tight")
plt.show()
print("\nDashboard salvo: dashboard_feedback.png")
criar_dashboard(dataset, hist_clf, acc_clf, acc_aspectos)
9. Sistema de Inferência Completo
class SistemaAnalise:
"""
Sistema completo de análise de feedback.
Interface unificada para todos os módulos.
"""
CLASSES_SENT = {0: "positivo", 1: "neutro", 2: "negativo"}
EMOJIS_SENT = {"positivo": "😊", "neutro": "😐",
"negativo": "😞"}
def __init__(self,
classificador: ClassificadorSentimentos,
extrator: ExtratorAspectos,
sumarizador: SumarizadorFeedback,
preprocessador: Preprocessador,
device: torch.device):
self.clf = classificador
self.ext = extrator
self.sum = sumarizador
self.prep = preprocessador
self.device = device
self.historico = []
def analisar(self, texto: str) -> dict:
"""
Análise completa de um único feedback.
"""
t_inicio = time.time()
# 1. Pré-processamento
ids = self.prep.transformar(texto)
tokens = torch.tensor(ids, dtype=torch.long)
tokens = tokens.unsqueeze(0).to(self.device)
# 2. Classificação de sentimento
self.clf.eval()
with torch.no_grad():
logits, pesos = self.clf(tokens)
probs = F.softmax(logits, dim=-1)
pred_idx = probs.argmax().item()
confianca = probs.max().item()
sentimento = self.CLASSES_SENT[pred_idx]
# 3. Extração de aspectos
aspectos = self.ext.extrair(texto)
# 4. Tokens relevantes (maior atenção)
pesos_np = pesos[0].cpu().numpy()
ids_validos = [i for i, id_ in enumerate(ids)
if id_ not in [0, 2, 3, 4]]
if ids_validos:
top_atencao = sorted(
[(self.prep.id2token.get(ids[i], "?"),
pesos_np[i])
for i in ids_validos],
key=lambda x: x[1], reverse=True
)[:5]
else:
top_atencao = []
t_fim = time.time()
resultado = {
"texto": texto,
"sentimento": sentimento,
"emoji": self.EMOJIS_SENT[sentimento],
"confianca": round(confianca, 4),
"probabilidades": {
self.CLASSES_SENT[i]: round(
probs[0][i].item(), 4
)
for i in range(3)
},
"aspectos": {
k: v for k, v in aspectos.items()
if v["detectado"]
},
"tokens_atentos": top_atencao,
"latencia_ms": round((t_fim - t_inicio) * 1000, 2)
}
self.historico.append(resultado)
return resultado
def analisar_lote(self, textos: list) -> list:
"""Analisa múltiplos feedbacks."""
return [self.analisar(t) for t in textos]
def gerar_relatorio(self,
resultados: list = None) -> str:
"""Gera relatório textual dos resultados."""
if resultados is None:
resultados = self.historico
n = len(resultados)
contagens = Counter(
r["sentimento"] for r in resultados
)
conf_media = np.mean(
[r["confianca"] for r in resultados]
)
lat_media = np.mean(
[r["latencia_ms"] for r in resultados]
)
return (
f"=== RELATÓRIO DE ANÁLISE ===\n"
f"Total analisado: {n}\n"
f"Positivos: {contagens['positivo']:3d} "
f"({contagens['positivo']/n*100:.1f}%)\n"
f"Neutros: {contagens['neutro']:3d} "
f"({contagens['neutro']/n*100:.1f}%)\n"
f"Negativos: {contagens['negativo']:3d} "
f"({contagens['negativo']/n*100:.1f}%)\n"
f"Confiança média: {conf_media:.4f}\n"
f"Latência média: {lat_media:.2f}ms\n"
)
# Instanciar sistema completo
sistema = SistemaAnalise(
classificador=clf,
extrator=extrator,
sumarizador=sumarizador,
preprocessador=prep,
device=device
)
# Demonstrar com exemplos reais
print("\n" + "=" * 65)
print("SISTEMA EM OPERAÇÃO — DEMONSTRAÇÃO")
print("=" * 65)
exemplos_demo = [
"O Smartphone XR Pro chegou muito rápido! Funciona perfeitamente e a câmera é excelente.",
"Fone Bluetooth razoável pelo preço. Bateria durou o esperado, qualidade mediana.",
"Terrível! O Notebook Ultra parou de funcionar em 2 semanas. Suporte não resolveu nada.",
"Amei o Smart Watch! Super recomendo, entrega foi rápida e o produto tem ótimo acabamento.",
]
for texto in exemplos_demo:
resultado = sistema.analisar(texto)
print(f"\n📝 Texto: '{texto[:55]}...'")
print(f" {resultado['emoji']} Sentimento: "
f"{resultado['sentimento'].upper()} "
f"(confiança: {resultado['confianca']:.2%})")
print(f" 📊 Probs: " + " | ".join(
f"{k}: {v:.2%}"
for k, v in resultado["probabilidades"].items()
))
if resultado["aspectos"]:
aspectos_str = ", ".join(resultado["aspectos"].keys())
print(f" 🏷️ Aspectos: {aspectos_str}")
if resultado["tokens_atentos"]:
tokens_str = ", ".join(
f"'{t}'" for t, _ in resultado["tokens_atentos"][:3]
)
print(f" 🔍 Tokens mais relevantes: {tokens_str}")
print(f" ⚡ Latência: {resultado['latencia_ms']:.1f}ms")
# Relatório do lote
print("\n" + sistema.gerar_relatorio())
10. Exportação e Persistência
print("\n" + "=" * 65)
print("EXPORTAÇÃO E PERSISTÊNCIA DO SISTEMA")
print("=" * 65)
import pickle
def salvar_sistema(sistema: SistemaAnalise,
caminho: str = "sistema_nlp"):
"""Salva todos os componentes do sistema."""
# Salvar pesos do classificador
torch.save(sistema.clf.state_dict(),
f"{caminho}_clf.pt")
# Salvar preprocessador
with open(f"{caminho}_prep.pkl", "wb") as f:
pickle.dump({
"token2id": sistema.prep.token2id,
"id2token": sistema.prep.id2token,
"max_vocab": sistema.prep.max_vocab,
"max_len": sistema.prep.max_len,
}, f)
# Salvar configuração
config = {
"vocab_size": len(sistema.prep.token2id),
"d_model": 128, "n_heads": 4,
"n_camadas": 3, "d_ff": 256,
"max_len": 64, "n_classes": 3,
"versao": "1.0.0",
"acuracia_teste": acc_clf,
}
with open(f"{caminho}_config.json", "w") as f:
json.dump(config, f, indent=2)
print(f"Sistema salvo em '{caminho}_*':")
import os
for ext in ["_clf.pt", "_prep.pkl", "_config.json"]:
p = f"{caminho}{ext}"
size = os.path.getsize(p) / 1024
print(f" {p}: {size:.1f} KB")
def carregar_sistema(caminho: str,
device: torch.device) -> SistemaAnalise:
"""Carrega sistema completo do disco."""
# Configuração
with open(f"{caminho}_config.json") as f:
config = json.load(f)
# Preprocessador
with open(f"{caminho}_prep.pkl", "rb") as f:
prep_data = pickle.load(f)
prep_carregado = Preprocessador(
max_vocab=prep_data["max_vocab"],
max_len=prep_data["max_len"]
)
prep_carregado.token2id = prep_data["token2id"]
prep_carregado.id2token = prep_data["id2token"]
prep_carregado.ajustado = True
# Classificador
clf_carregado = ClassificadorSentimentos(
vocab_size=config["vocab_size"],
d_model=config["d_model"],
n_heads=config["n_heads"],
n_camadas=config["n_camadas"],
d_ff=config["d_ff"],
max_len=config["max_len"],
n_classes=config["n_classes"]
).to(device)
clf_carregado.load_state_dict(
torch.load(f"{caminho}_clf.pt",
weights_only=True,
map_location=device)
)
return SistemaAnalise(
classificador=clf_carregado,
extrator=ExtratorAspectos().ajustar(dataset),
sumarizador=SumarizadorFeedback(),
preprocessador=prep_carregado,
device=device
)
# Salvar e recarregar
salvar_sistema(sistema)
sistema_recarregado = carregar_sistema("sistema_nlp", device)
# Verificar que o sistema recarregado funciona
print("\nVerificação do sistema recarregado:")
texto_teste = "Produto excelente! Chegou rápido e funciona perfeitamente."
r1 = sistema.analisar(texto_teste)
r2 = sistema_recarregado.analisar(texto_teste)
print(f" Original: {r1['sentimento']} ({r1['confianca']:.4f})")
print(f" Recarregado: {r2['sentimento']} ({r2['confianca']:.4f})")
print(f" Idênticos: {r1['sentimento'] == r2['sentimento']}")
# Relatório final completo
print("\n" + "=" * 65)
print("MÉTRICAS FINAIS DO SISTEMA")
print("=" * 65)
# Métricas de classificação
print(f"\n1. Classificador de Sentimentos:")
print(f" Acurácia: {acc_clf:.4f}")
print(f" Parâmetros: {sum(p.numel() for p in clf.parameters()):,}")
# Métricas do extrator
print(f"\n2. Extrator de Aspectos:")
for asp, acc in acc_aspectos.items():
print(f" {asp:<12}: {acc:.4f}")
# Throughput
tempos = [sistema.analisar(f.texto)["latencia_ms"]
for f in dataset[:50]]
print(f"\n3. Performance de Inferência (n=50):")
print(f" Latência média: {np.mean(tempos):.2f}ms")
print(f" Latência p95: {np.percentile(tempos, 95):.2f}ms")
print(f" Throughput: ~{1000/np.mean(tempos):.0f} req/s")
# Visualização final de resultados
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# Matriz de confusão final
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(labels_te, preds_te)
sns.heatmap(
cm, annot=True, fmt="d", cmap="Blues",
xticklabels=["Pos", "Neu", "Neg"],
yticklabels=["Pos", "Neu", "Neg"],
ax=axes[0]
)
axes[0].set_title(f"Matriz de Confusão\n(Acc={acc_clf:.4f})")
axes[0].set_ylabel("Real")
axes[0].set_xlabel("Predito")
# Distribuição de confiança
todas_conf = [r["confianca"]
for r in [sistema.analisar(f.texto)
for f in dataset[:100]]]
axes[1].hist(todas_conf, bins=20, color="steelblue",
alpha=0.7, edgecolor="white")
axes[1].set_xlabel("Confiança da Predição")
axes[1].set_ylabel("Frequência")
axes[1].set_title("Distribuição de Confiança")
# Loss curves finais
ep = range(1, len(hist_clf["tr_loss"]) + 1)
axes[2].plot(ep, hist_clf["tr_loss"], color="steelblue",
linewidth=2, label="Treino")
axes[2].plot(ep, hist_clf["va_loss"], color="coral",
linewidth=2, linestyle="--", label="Validação")
axes[2].set_xlabel("Época")
axes[2].set_ylabel("Loss")
axes[2].set_title("Curvas de Loss Final")
axes[2].legend()
plt.suptitle("Projeto Final — Métricas do Sistema Completo",
fontsize=14)
plt.tight_layout()
plt.savefig("metricas_finais.png", dpi=150,
bbox_inches="tight")
plt.show()
print("\n" + "=" * 65)
print("PROJETO CONCLUÍDO!")
print("=" * 65)
print("""
Componentes entregues:
✓ Dataset de 2000 feedbacks sintéticos realistas
✓ Pipeline de pré-processamento com vocabulário aprendido
✓ Classificador de sentimentos (Transformer + atenção)
✓ Extrator de aspectos baseado em regras + co-ocorrência
✓ Sumarizador estatístico de grupos de feedbacks
✓ Dashboard analítico completo (7 visualizações)
✓ Sistema de inferência com latência monitorada
✓ Persistência completa (pesos + configuração + vocab)
✓ Verificação de carga e funcionamento
Extensões possíveis para produção:
→ Substituir classificador por BERT fine-tuned (Aula 27)
→ Adicionar LoRA para fine-tuning eficiente (Aula 28)
→ Deploy com FastAPI + Docker
→ Monitoramento de drift com evidently
→ CI/CD com GitHub Actions
→ A/B testing entre modelos
""")
Resumo da Aula
- Um projeto de NLP completo integra pré-processamento, modelagem, avaliação e deploy em um pipeline coerente
- A estrutura de dados (dataclass) define claramente o esquema antes de qualquer código de ML
- O preprocessador deve ser salvo junto com o modelo — vocabulário e parâmetros de normalização fazem parte do sistema
- Atenção sobre tokens (attention pooling) frequentemente supera mean pooling para classificação de textos curtos
- Extratores baseados em regras são complementares a modelos neurais — rápidos, interpretáveis e fáceis de manter
- Sumarizadores baseados em estatísticas são úteis como baseline antes de usar modelos generativos
- Dashboard analítico é essencial para comunicar resultados a stakeholders não técnicos
- Métricas de performance operacional (latência, throughput) são tão importantes quanto métricas de ML
- O sistema deve ser carregável de forma reproduzível: config + pesos + preprocessador sempre juntos
- Em produção: monitorar drift, implementar A/B testing e ter estratégia de re-treino periódico
Exercícios
-
O sistema atual usa regras para extração de aspectos. Como você substituiria este módulo por um modelo de token classification treinado? Quais dados de treino seriam necessários e como você avaliaria a qualidade?
✓ Resposta:Para substituir por token classification, cada token do texto receberia um label de aspecto (ou "O" para outside no esquema BIO). Por exemplo: "a [bateria-B-produto] dura [pouco-I-produto] e a [entrega-B-entrega] [atrasou-I-entrega]".
Dados necessários: revisar manualmente 1000-2000 feedbacks anotando tokens com aspectos usando ferramenta como Label Studio. Criar o esquema de anotação: B-produto, I-produto, B-entrega, I-entrega, etc. Garantir pelo menos 200 exemplos por categoria.
Modelo:
AutoModelForTokenClassificationcom BERT fine-tuned. Avaliação por F1 de span: um span é correto apenas se todos os tokens estiverem corretos, não token individual.Vantagens sobre regras: captura expressões não previstas, generaliza para novas formas de dizer o mesmo aspecto, não precisa de manutenção manual quando novos padrões surgem.
-
O sumarizador usa estatísticas simples. Descreva como você implementaria um sumarizador generativo usando um modelo de linguagem pequeno (ex: T5-small) fine-tuned em pares (conjunto de reviews, resumo). Quais seriam os desafios?
✓ Resposta:Para sumarização generativa, precisaria de pares (lista_de_reviews → resumo_humano). Pode-se criar esses pares de forma semi-automática: pedir a analistas que escrevam resumos de grupos de 10-20 reviews reais, ou usar GPT-4 para gerar resumos de alta qualidade que humanos depois revisam.
Fine-tuning do T5-small no formato: input = concatenação das reviews (truncadas), output = resumo. Desafios principais: (1) truncamento — grupos de 10 reviews podem exceder o contexto do T5 (512 tokens), exigindo estratégias como sumarização hierárquica; (2) avaliação — ROUGE captura sobreposição de n-gramas mas não qualidade semântica, sendo necessária avaliação humana; (3) alucinações — o modelo pode inventar fatos não presentes nas reviews.
-
Como você adicionaria detecção de tópicos (topic modeling) ao sistema? Compare LDA (Latent Dirichlet Allocation) com abordagem neural baseada em clustering de embeddings. Quais são os trade-offs?
✓ Resposta:LDA modela cada documento como mistura de tópicos, onde cada tópico é distribuição sobre palavras. Não requer dados rotulados, interpretável (top palavras por tópico), mas assume bag-of-words (ignora semântica e ordem), número de tópicos deve ser definido manualmente e é sensível a pré-processamento.
Abordagem neural: compute embeddings (BERT ou sentence-transformers) para cada review, aplique UMAP para redução dimensional, depois HDBSCAN ou K-Means para clustering. Os clusters correspondem a tópicos. Vantagens: captura semântica real, funciona com textos curtos onde LDA falha, não precisa definir número de clusters. Desvantagem: menos interpretável, clusters precisam ser nomeados manualmente.
Trade-off principal: LDA é mais interpretável e determinístico; clustering neural é mais preciso semanticamente mas é caixa preta. Para comunicar a stakeholders, LDA. Para performance máxima em segmentação, neural.
-
O sistema atual é stateless — cada análise é independente. Como você adicionaria aprendizado contínuo para adaptar o modelo a novos padrões de linguagem sem re-treinar do zero? Quais são os riscos?
✓ Resposta:Aprendizado contínuo (continual learning) é possível com algumas estratégias. Online learning: re-treinar periodicamente (semanal/mensal) com novos dados coletados. Replay: misturar novos dados com amostra dos dados originais para evitar catastrophic forgetting. EWC: adicionar penalidade que preserva pesos importantes para tarefas anteriores.
Implementação prática: criar pipeline de coleta de dados em produção com validação humana de amostras (active learning — selecionar os exemplos mais incertos para anotação), re-treinar automaticamente quando drift for detectado, usar A/B testing para validar o novo modelo antes de deploy completo.
Riscos: (1) data poisoning — feedbacks adversariais podem degradar o modelo; (2) catastrophic forgetting — modelo perde performance em padrões antigos; (3) label drift — a definição de "positivo" pode mudar sutilmente com o tempo. Mitigações: validação humana de amostras de treino, testes de regressão automatizados antes de cada deploy, monitoramento contínuo de métricas.
-
Descreva como você faria o deploy deste sistema em produção usando FastAPI. Quais endpoints você criaria, como gerenciaria concorrência, e como implementaria caching para feedbacks repetidos?
✓ Resposta:Endpoints principais: -
POST /analyze→ análise de texto único, retorna sentimento + aspectos + confiança -POST /analyze/batch→ lista de textos, retorna lista de resultados -GET /health→ status do serviço, latência média, número de requests -GET /metrics→ dashboard de métricas agregadasGerenciamento de concorrência: carregar o modelo uma vez no startup (não por request), usar
asynciopara requests assíncronos, pool de workers para inference paralela comtorch.multiprocessing. Para alto throughput: batching dinâmico — acumular requests por ~5ms e processar em batch.Caching: Redis com TTL de 1 hora para textos repetidos (hash do texto como chave). Útil para sistemas de e-commerce onde reviews populares são consultadas repetidamente. Cache invalidation quando modelo é atualizado.
# Estrutura básica @app.post("/analyze") async def analyze(request: AnalyzeRequest): cache_key = hashlib.md5(request.text.encode()).hexdigest() if cached := await redis.get(cache_key): return json.loads(cached) result = sistema.analisar(request.text) await redis.setex(cache_key, 3600, json.dumps(result)) return result -
O sistema foi treinado em dados sintéticos. Quais problemas você esperaria ao aplicá-lo em dados reais de e-commerce? Como você mediria e mitigaria o covariate shift entre dados de treino e produção?
✓ Resposta:Problemas esperados com dados reais: (1) covariate shift — linguagem real tem gírias, erros de ortografia, emojis, mistura de idiomas, abreviações não presentes no treino sintético; (2) distribuição de classes diferente — proporção real de positivo/neutro/negativo pode ser bem diferente dos 55/25/20% sintéticos; (3) textos mais curtos ou longos que o esperado; (4) domínio específico de produtos com vocabulário não visto.
Medição de drift: monitorar distribuição dos logits (se o modelo está ficando mais incerto), distribuição das classes preditas (se mudou muito do esperado), performance em amostra anotada mensalmente.
Mitigações: (1) coletar 500-1000 reviews reais para fine-tuning inicial antes de deploy; (2) aplicar augmentation de texto realista durante treino (inserir erros, emojis, abreviações); (3) usar modelo pré-treinado em português real (neuralmind/bert-base-portuguese-cased) como backbone ao invés de treinar do zero; (4) implementar feedback loop — quando confiança < 0.6, enviar para revisão humana e usar como dado de treino.
Referências
- Hugging Face — Pipelines e Transformers
- FastAPI — Framework para APIs Python
- Gradio — Interface web para ML
- Evidently — Monitoramento de drift
- Label Studio — Ferramenta de anotação
- Sentence Transformers — Embeddings de qualidade
- BERTopic — Topic modeling com BERT
- Redis — Cache em produção
- Docker — Containerização de aplicações
- ML System Design (Chip Huyen)