Projetos
Passo a passo · Deep Learning · Tarefa 04

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.

Visão geral

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.

Ferramentas

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 Complaintsclaritystorm/cfpb-consumer-complaints (HuggingFace). Modelo: Qwen/Qwen2.5-0.5B-Instruct — pequeno o suficiente para rodar em Colab gratuito.

Passo 1

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.

Passo 2

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.

Passo 3

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.

Passo 4

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.

Passo 5

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.

Recursos

Código completo e referências

github.com/Jair-pc/puc-minas-arquiteturas_de_deep_learning

Dataset: CFPB Consumer Complaintsclaritystorm/cfpb-consumer-complaints (HuggingFace). Modelo: Qwen/Qwen2.5-0.5B-Instruct.

Anterior: RNNs Todos os projetos