Central de Atendimento com LLMs e LoRA
Como o ChatGPT entende uma frase? Vamos ver o mecanismo de atenção funcionar na prática e depois fazer fine-tuning de um LLM real (Qwen 2.5) para responder reclamações financeiras — gastando apenas ~1% dos parâmetros com LoRA.
O que é um Transformer?
Imagina que você está lendo: "O banco estava cheio de água." Sua mente pergunta: banco de rio ou banco de sentar? Para resolver isso, você "olha" para a palavra "água" — ela dá o contexto.
É exatamente isso que o mecanismo de atenção faz: para cada palavra, calcula o quanto cada outra palavra importa. Esse mecanismo é o coração dos Transformers — a arquitetura por trás do ChatGPT, Gemini, Claude e Qwen.
Aqui vamos: entender a atenção visualmente → aplicar o Qwen 2.5 em zero-shot → fazer fine-tuning eficiente com LoRA → comparar antes e depois.
O que você vai precisar
- Python 3.10+
- PyTorch — base do HuggingFace.
- HuggingFace Transformers — para carregar o Qwen 2.5.
- HuggingFace Datasets — para o CFPB.
- PEFT — para aplicar LoRA.
- GPU recomendada — Google Colab (T4 grátis) ou Kaggle Notebooks.
!pip -q install torch transformers datasets accelerate sentencepiece peft
print("Instalacao concluida!")
print("Reinicie o kernel antes de continuar.")
Dataset: CFPB Consumer Complaints — claritystorm/cfpb-consumer-complaints (HuggingFace). Modelo: Qwen/Qwen2.5-0.5B-Instruct — pequeno o suficiente para rodar em Colab gratuito.
Visualizar o mecanismo de atenção
Antes de usar o LLM, vamos ver a atenção com um exemplo didático — os pesos mostram quais palavras o modelo "foca" ao processar cada token.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Exemplo: pesos de atenção da frase "O atendimento foi ruim..."
palavras = ["O", "atendimento", "foi", "ruim", "mas", "o", "produto", "chegou", "rapido"]
pesos = [0.02, 0.18, 0.03, 0.28, 0.10, 0.02, 0.12, 0.07, 0.18]
df_atencao = pd.DataFrame({"palavra": palavras, "peso_de_atencao": pesos})
print(df_atencao.to_string(index=False))
print(f"\nSoma dos pesos: {sum(pesos):.2f} (atencao sempre soma 1.0)")
"ruim" (0.28) recebe mais atenção — faz sentido para classificar como reclamação negativa sobre atendimento.
# Self-attention: "rapido" observando as outras palavras
palavra_foco = "rapido"
palavras_obs = ["atendimento", "ruim", "produto", "chegou", "rapido", "mas"]
pesos_rapido = [0.05, 0.05, 0.25, 0.35, 0.25, 0.05]
print(f"Self-attention de '{palavra_foco}':")
for p, w in zip(palavras_obs, pesos_rapido):
print(f" {p:15} → {w:.2f}")
# "chegou" (0.35) e "produto" (0.25) dao contexto para "rapido"
# Simulação matemática: Q·K^T / sqrt(d_k) → softmax
np.random.seed(7)
n_tokens, d_k = 5, 4
Q = np.random.randn(n_tokens, d_k) # queries: "o que esta palavra procura?"
K = np.random.randn(n_tokens, d_k) # keys: "o que esta palavra oferece?"
scores = Q @ K.T / np.sqrt(d_k) # sqrt(d_k) estabiliza os gradientes
def softmax(x):
e = np.exp(x - x.max(axis=-1, keepdims=True))
return e / e.sum(axis=-1, keepdims=True)
atencao = softmax(scores)
print("Matriz de atencao (5x5):")
print(np.round(atencao, 3))
O que descobrimos? Atenção = multiplicação de matrizes + softmax. Simples matematicamente, poderoso para capturar relações entre palavras a qualquer distância na frase.
Carregar o Qwen 2.5 e testar zero-shot
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
MODELO = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(MODELO)
model = AutoModelForCausalLM.from_pretrained(
MODELO,
torch_dtype=torch.float16, # metade da memoria vs float32
device_map="auto" # coloca na GPU automaticamente
)
def classificar(texto, model_atual):
prompt = f"""Analise a reclamacao e responda:
1. Categoria: [produto/entrega/cobranca/atendimento/outro]
2. Urgencia: [baixa/media/alta]
3. Resposta empatica ao cliente (2-3 frases)
Reclamacao: {texto}
Resposta:"""
inputs = tokenizer(prompt, return_tensors="pt").to(model_atual.device)
with torch.no_grad():
outputs = model_atual.generate(**inputs, max_new_tokens=200, temperature=0.7, do_sample=True)
return tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
# Teste com reclamacao real
reclamacao = "My credit card was charged twice for the same purchase. Nobody from customer service helped me resolve this."
print(classificar(reclamacao, model))
O que descobrimos? Zero-shot funciona mas sem consistência de formato e sem vocabulário do domínio financeiro. O fine-tuning com LoRA vai resolver isso.
Preparar o dataset instrucional
Para o fine-tuning supervisionado, transformamos as reclamações do CFPB em pares instrução → resposta.
from datasets import load_dataset
# Carrega reclamacoes publicas do Consumer Financial Protection Bureau
dataset = load_dataset("claritystorm/cfpb-consumer-complaints", split="train")
df_cfpb = dataset.to_pandas()
# Filtra registros com texto de reclamacao
amostra = df_cfpb[df_cfpb['Consumer complaint narrative'].notna()].head(500)
def formatar_instrucao(row):
return f"""### Instrucao:
Analise a reclamacao financeira. Informe categoria, urgencia e resposta empatica.
### Reclamacao:
{str(row['Consumer complaint narrative'])[:300]}
### Resposta:
Categoria: {row['Product']}
Urgencia: alta
Resposta: Lamentamos o ocorrido. Entraremos em contato em ate 48 horas para resolver."""
amostra['texto'] = amostra.apply(formatar_instrucao, axis=1)
print(f"Dataset instrucional: {len(amostra)} exemplos")
O que descobrimos? Transformamos reclamações brutas em pares instrução/resposta. O modelo vai aprender a imitar esse formato específico.
Fine-tuning com LoRA — treinar 1% dos parâmetros
LoRA adiciona matrizes pequenas ("adaptadores") em paralelo às camadas de atenção. O modelo base fica congelado — só os adaptadores são atualizados.
from peft import LoraConfig, get_peft_model, TaskType
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM, # geracao de texto
r=8, # rank: menor = menos parametros
lora_alpha=16, # escala (geralmente 2x o rank)
target_modules=["q_proj", "v_proj"], # camadas de atencao a adaptar
lora_dropout=0.1,
bias="none"
)
model_lora = get_peft_model(model, lora_config)
model_lora.print_trainable_parameters()
# trainable params: ~1.2M || all params: ~500M || trainable%: 0.24%
from transformers import TrainingArguments, Trainer, DataCollatorForLanguageModeling
training_args = TrainingArguments(
output_dir="qwen_ft_atendimento",
num_train_epochs=3,
per_device_train_batch_size=4,
learning_rate=2e-4,
fp16=True,
logging_steps=10,
save_strategy="epoch",
report_to="none"
)
trainer = Trainer(
model=model_lora,
args=training_args,
train_dataset=train_dataset,
data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False)
)
trainer.train()
print("Fine-tuning concluido!")
O que descobrimos? Com apenas 1,2M parâmetros treináveis (de 500M), o LoRA adaptou o Qwen ao domínio financeiro em poucos minutos de treino. Os adaptadores LoRA são salvos separadamente — o modelo base permanece intacto.
Comparar antes e depois do fine-tuning
reclamacao_teste = """I have been trying to dispute a fraudulent charge on my account
for three months. Every time I call, I am transferred to different departments
and nobody resolves the issue. The charge is for $847.50."""
print("=== MODELO BASE (zero-shot) ===")
print(classificar(reclamacao_teste, model))
print("\n=== MODELO FINE-TUNED COM LoRA ===")
model_lora.eval()
inputs = tokenizer(
f"### Instrucao:\nAnalise a reclamacao financeira. Informe categoria, urgencia e resposta empatica.\n\n### Reclamacao:\n{reclamacao_teste}\n\n### Resposta:\n",
return_tensors="pt"
).to(model_lora.device)
with torch.no_grad():
outputs = model_lora.generate(**inputs, max_new_tokens=150)
print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True))
O modelo fine-tuned gera respostas aderentes ao formato, usa terminologia financeira correta e mantém o tom empático definido no dataset instrucional — diferença clara do zero-shot.
O que descobrimos? LoRA é a forma mais acessível de personalizar LLMs. Com GPU de Colab e 500 exemplos, é possível adaptar um modelo de linguagem a qualquer domínio específico.
Código completo e referências
github.com/Jair-pc/puc-minas-arquiteturas_de_deep_learning
Dataset: CFPB Consumer Complaints — claritystorm/cfpb-consumer-complaints (HuggingFace). Modelo: Qwen/Qwen2.5-0.5B-Instruct.